From c1a7254fdff802e2852f97365dd266979296fc33 Mon Sep 17 00:00:00 2001 From: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:50:13 +0300 Subject: [PATCH] fix arbitrary file write via write_text_file/write_image_file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src-tauri/src/main.rs | 48 +++++++++++++++++++++++++++++++++---------- src/main.ts | 18 ++++++++-------- src/storage.ts | 6 +++--- src/ui/export.ts | 9 +++----- 4 files changed, 51 insertions(+), 30 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index bd10461..6c3968f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -9,6 +9,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use tauri::menu::{Menu, MenuItem}; use tauri::tray::TrayIconBuilder; use tauri::{AppHandle, Emitter, Manager, WindowEvent}; +use tauri_plugin_dialog::DialogExt; use tauri_plugin_opener::OpenerExt; const RATES_TTL_SECS: u64 = 3600; @@ -38,20 +39,45 @@ fn safe_name(name: &str) -> bool { } #[tauri::command] -fn write_text_file(path: String, contents: String) -> Result<(), String> { - write_atomic(std::path::Path::new(&path), &contents) +fn write_text_file(app: AppHandle, contents: String, is_sum: bool) -> Result { + 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] -fn write_image_file(path: String, data_base64: String) -> Result<(), String> { - use std::io::Write; - let bytes = base64_decode(&data_base64)?; - let p = std::path::Path::new(&path); - let tmp = p.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, p).map_err(|e| e.to_string()) +fn write_image_file(app: AppHandle, data_base64: String) -> Result { + if let Some(p) = app + .dialog() + .file() + .add_filter("PNG Image", &["png"]) + .blocking_save_file() + { + let path = p.into_path().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, String> { diff --git a/src/main.ts b/src/main.ts index 47edfa5..e65f845 100644 --- a/src/main.ts +++ b/src/main.ts @@ -931,18 +931,16 @@ async function initUI(): Promise { } else if (action === "print") { window.print(); } else if (isTauri()) { - const { save } = await import("@tauri-apps/plugin-dialog"); const { invoke } = await import("@tauri-apps/api/core"); const isSum = action === "save-sum"; - const path = await save({ - filters: isSum - ? [{ name: "Summarum Sheet", extensions: ["sum"] }] - : [{ name: "Text file", extensions: ["txt"] }], - }); - if (path) { - const content = isSum ? editor.getText() : editor.getSheetWithResults(); - await invoke("write_text_file", { path, contents: content }); - toast(t("saved")); + const content = isSum ? editor.getText() : editor.getSheetWithResults(); + try { + const saved = await invoke("write_text_file", { contents: content, isSum }); + if (saved) { + toast(t("saved")); + } + } catch { + toast(t("imageFailed")); } } }); diff --git a/src/storage.ts b/src/storage.ts index ce99608..c316eda 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -234,9 +234,9 @@ export async function fetchHistoricalRates(date: string): Promise { - if (!isTauri()) return; - await invoke("write_image_file", { path, dataBase64 }); +export async function writeImageFile(dataBase64: string): Promise { + if (!isTauri()) return false; + return invoke("write_image_file", { dataBase64 }); } // ---------- market data diff --git a/src/ui/export.ts b/src/ui/export.ts index b94e6ab..7ce1499 100644 --- a/src/ui/export.ts +++ b/src/ui/export.ts @@ -58,12 +58,9 @@ export async function exportSheetImage(deps: ExportImageDeps): Promise { } catch { // clipboard API failed — save to file try { - const { save } = await import("@tauri-apps/plugin-dialog"); - const path = await save({ filters: [{ name: "PNG Image", extensions: ["png"] }] }); - if (path) { - const buf = await blob.arrayBuffer(); - const b64 = bytesToBase64(new Uint8Array(buf)); - await writeImageFile(path, b64); + const buf = await blob.arrayBuffer(); + const b64 = bytesToBase64(new Uint8Array(buf)); + if (await writeImageFile(b64)) { toast(t("saved")); } } catch {