mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-10 19:11:58 +03:00
fix arbitrary file write via write_text_file/write_image_file
Both Tauri commands took a raw path string straight from the frontend and wrote to it with no validation — any code able to call invoke() (a compromised or malicious extension script, which run with full page privileges) could write to any path the OS user has access to, not just one chosen through the save dialog. Move the save-file dialog into the Rust command itself so the only path ever used is one the native dialog returned; the frontend now only sends the content to save.
This commit is contained in:
parent
f2deaa1b94
commit
c1a7254fdf
4 changed files with 51 additions and 30 deletions
|
|
@ -9,6 +9,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tauri::menu::{Menu, MenuItem};
|
use tauri::menu::{Menu, MenuItem};
|
||||||
use tauri::tray::TrayIconBuilder;
|
use tauri::tray::TrayIconBuilder;
|
||||||
use tauri::{AppHandle, Emitter, Manager, WindowEvent};
|
use tauri::{AppHandle, Emitter, Manager, WindowEvent};
|
||||||
|
use tauri_plugin_dialog::DialogExt;
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
|
||||||
const RATES_TTL_SECS: u64 = 3600;
|
const RATES_TTL_SECS: u64 = 3600;
|
||||||
|
|
@ -38,20 +39,45 @@ fn safe_name(name: &str) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn write_text_file(path: String, contents: String) -> Result<(), String> {
|
fn write_text_file(app: AppHandle, contents: String, is_sum: bool) -> Result<bool, String> {
|
||||||
write_atomic(std::path::Path::new(&path), &contents)
|
let (name, ext) = if is_sum {
|
||||||
|
("Summarum Sheet", "sum")
|
||||||
|
} else {
|
||||||
|
("Text file", "txt")
|
||||||
|
};
|
||||||
|
if let Some(p) = app
|
||||||
|
.dialog()
|
||||||
|
.file()
|
||||||
|
.add_filter(name, &[ext])
|
||||||
|
.blocking_save_file()
|
||||||
|
{
|
||||||
|
write_atomic(&p.into_path().map_err(|e| e.to_string())?, &contents)?;
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn write_image_file(path: String, data_base64: String) -> Result<(), String> {
|
fn write_image_file(app: AppHandle, data_base64: String) -> Result<bool, String> {
|
||||||
use std::io::Write;
|
if let Some(p) = app
|
||||||
let bytes = base64_decode(&data_base64)?;
|
.dialog()
|
||||||
let p = std::path::Path::new(&path);
|
.file()
|
||||||
let tmp = p.with_extension("tmp");
|
.add_filter("PNG Image", &["png"])
|
||||||
std::fs::File::create(&tmp)
|
.blocking_save_file()
|
||||||
.and_then(|mut f| f.write_all(&bytes))
|
{
|
||||||
.map_err(|e| e.to_string())?;
|
let path = p.into_path().map_err(|e| e.to_string())?;
|
||||||
fs::rename(&tmp, p).map_err(|e| e.to_string())
|
use std::io::Write;
|
||||||
|
let bytes = base64_decode(&data_base64)?;
|
||||||
|
let tmp = path.with_extension("tmp");
|
||||||
|
std::fs::File::create(&tmp)
|
||||||
|
.and_then(|mut f| f.write_all(&bytes))
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
fs::rename(&tmp, path).map_err(|e| e.to_string())?;
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base64_decode(s: &str) -> Result<Vec<u8>, String> {
|
fn base64_decode(s: &str) -> Result<Vec<u8>, String> {
|
||||||
|
|
|
||||||
18
src/main.ts
18
src/main.ts
|
|
@ -931,18 +931,16 @@ async function initUI(): Promise<void> {
|
||||||
} else if (action === "print") {
|
} else if (action === "print") {
|
||||||
window.print();
|
window.print();
|
||||||
} else if (isTauri()) {
|
} else if (isTauri()) {
|
||||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
|
||||||
const { invoke } = await import("@tauri-apps/api/core");
|
const { invoke } = await import("@tauri-apps/api/core");
|
||||||
const isSum = action === "save-sum";
|
const isSum = action === "save-sum";
|
||||||
const path = await save({
|
const content = isSum ? editor.getText() : editor.getSheetWithResults();
|
||||||
filters: isSum
|
try {
|
||||||
? [{ name: "Summarum Sheet", extensions: ["sum"] }]
|
const saved = await invoke<boolean>("write_text_file", { contents: content, isSum });
|
||||||
: [{ name: "Text file", extensions: ["txt"] }],
|
if (saved) {
|
||||||
});
|
toast(t("saved"));
|
||||||
if (path) {
|
}
|
||||||
const content = isSum ? editor.getText() : editor.getSheetWithResults();
|
} catch {
|
||||||
await invoke("write_text_file", { path, contents: content });
|
toast(t("imageFailed"));
|
||||||
toast(t("saved"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -234,9 +234,9 @@ export async function fetchHistoricalRates(date: string): Promise<Record<string,
|
||||||
|
|
||||||
// ---------- image export
|
// ---------- image export
|
||||||
|
|
||||||
export async function writeImageFile(path: string, dataBase64: string): Promise<void> {
|
export async function writeImageFile(dataBase64: string): Promise<boolean> {
|
||||||
if (!isTauri()) return;
|
if (!isTauri()) return false;
|
||||||
await invoke("write_image_file", { path, dataBase64 });
|
return invoke("write_image_file", { dataBase64 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- market data
|
// ---------- market data
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,9 @@ export async function exportSheetImage(deps: ExportImageDeps): Promise<void> {
|
||||||
} catch {
|
} catch {
|
||||||
// clipboard API failed — save to file
|
// clipboard API failed — save to file
|
||||||
try {
|
try {
|
||||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
const buf = await blob.arrayBuffer();
|
||||||
const path = await save({ filters: [{ name: "PNG Image", extensions: ["png"] }] });
|
const b64 = bytesToBase64(new Uint8Array(buf));
|
||||||
if (path) {
|
if (await writeImageFile(b64)) {
|
||||||
const buf = await blob.arrayBuffer();
|
|
||||||
const b64 = bytesToBase64(new Uint8Array(buf));
|
|
||||||
await writeImageFile(path, b64);
|
|
||||||
toast(t("saved"));
|
toast(t("saved"));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue