mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 02:12:00 +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::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<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)
|
||||
}
|
||||
}
|
||||
|
||||
#[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> {
|
||||
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 p = std::path::Path::new(&path);
|
||||
let tmp = p.with_extension("tmp");
|
||||
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, p).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> {
|
||||
|
|
|
|||
14
src/main.ts
14
src/main.ts
|
|
@ -931,19 +931,17 @@ async function initUI(): Promise<void> {
|
|||
} 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 });
|
||||
try {
|
||||
const saved = await invoke<boolean>("write_text_file", { contents: content, isSum });
|
||||
if (saved) {
|
||||
toast(t("saved"));
|
||||
}
|
||||
} catch {
|
||||
toast(t("imageFailed"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -234,9 +234,9 @@ export async function fetchHistoricalRates(date: string): Promise<Record<string,
|
|||
|
||||
// ---------- image export
|
||||
|
||||
export async function writeImageFile(path: string, dataBase64: string): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
await invoke("write_image_file", { path, dataBase64 });
|
||||
export async function writeImageFile(dataBase64: string): Promise<boolean> {
|
||||
if (!isTauri()) return false;
|
||||
return invoke("write_image_file", { dataBase64 });
|
||||
}
|
||||
|
||||
// ---------- market data
|
||||
|
|
|
|||
|
|
@ -58,12 +58,9 @@ export async function exportSheetImage(deps: ExportImageDeps): Promise<void> {
|
|||
} 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);
|
||||
if (await writeImageFile(b64)) {
|
||||
toast(t("saved"));
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue