mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 02:12:00 +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::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
|
|
@ -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"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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