mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 01:02:01 +03:00
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:
parent
984275711a
commit
6c2cd9fda2
5 changed files with 103 additions and 30 deletions
52
patch_returns.diff
Normal file
52
patch_returns.diff
Normal 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> {
|
||||
|
|
@ -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> {
|
||||
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<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)
|
||||
}
|
||||
}
|
||||
|
||||
fn base64_decode(s: &str) -> Result<Vec<u8>, String> {
|
||||
|
|
|
|||
18
src/main.ts
18
src/main.ts
|
|
@ -907,18 +907,16 @@ async function boot(): 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 });
|
||||
toast(t("saved"));
|
||||
const content = isSum ? editor.getText() : editor.getSheetWithResults();
|
||||
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);
|
||||
const buf = await blob.arrayBuffer();
|
||||
const b64 = bytesToBase64(new Uint8Array(buf));
|
||||
if (await writeImageFile(b64)) {
|
||||
toast(t("saved"));
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue