Fix arbitrary file write vulnerability by moving file dialog to backend

Moved the file selection dialog (`tauri-plugin-dialog`) into the Rust
backend commands (`write_image_file` and `write_text_file`) instead of
having the frontend supply arbitrary raw paths to write to. This ensures
that the OS-native dialog handles secure path resolution and prevents
potential remote code execution (RCE) via XSS.

Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
This commit is contained in:
google-labs-jules[bot] 2026-07-08 12:00:02 +00:00
parent 984275711a
commit 6c2cd9fda2
5 changed files with 103 additions and 30 deletions

52
patch_returns.diff Normal file
View file

@ -0,0 +1,52 @@
--- src-tauri/src/main.rs
+++ src-tauri/src/main.rs
@@ -40,40 +40,42 @@
}
#[tauri::command]
-fn write_text_file(app: AppHandle, contents: String, is_sum: bool) -> Result<(), String> {
+fn write_text_file(app: AppHandle, contents: String, is_sum: bool) -> Result<bool, String> {
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)
}
- Ok(())
}
#[tauri::command]
-fn write_image_file(app: AppHandle, data_base64: String) -> Result<(), String> {
+fn write_image_file(app: AppHandle, data_base64: String) -> Result<bool, String> {
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)
}
- Ok(())
}
fn base64_decode(s: &str) -> Result<Vec<u8>, String> {

View file

@ -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> {

View file

@ -907,18 +907,16 @@ async function boot(): 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"));
} }
} }
}); });

View file

@ -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

View file

@ -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 {