mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 02:12:00 +03:00
fix(security): prevent arbitrary file system operations in migrate_data_dir
Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
This commit is contained in:
parent
c958f30662
commit
f6d12097b9
3 changed files with 44 additions and 20 deletions
|
|
@ -5,8 +5,32 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::menu::{Menu, MenuItem};
|
||||
|
||||
struct MigrationState(Mutex<Option<PathBuf>>);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PickDataDirResult {
|
||||
path: String,
|
||||
has_documents: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn pick_data_dir(app: AppHandle, state: tauri::State<'_, MigrationState>) -> Result<Option<PickDataDirResult>, String> {
|
||||
if let Some(p) = app.dialog().file().blocking_pick_folder() {
|
||||
let path = p.into_path().map_err(|e| e.to_string())?;
|
||||
let has_docs = path.join("documents.json").exists();
|
||||
*state.0.lock().unwrap() = Some(path.clone());
|
||||
Ok(Some(PickDataDirResult {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
has_documents: has_docs,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri::{AppHandle, Emitter, Manager, WindowEvent};
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
|
@ -537,11 +561,6 @@ fn open_backups_folder(app: AppHandle, dir: Option<String>) -> Result<(), String
|
|||
|
||||
// ---------- data folder migration
|
||||
|
||||
#[tauri::command]
|
||||
fn data_dir_has_documents(dir: String) -> bool {
|
||||
PathBuf::from(dir).join("documents.json").exists()
|
||||
}
|
||||
|
||||
/// strategy: "move" (copy mine, delete originals), "overwrite" (same, target
|
||||
/// had documents), "use_existing" (just switch — keep the target's files)
|
||||
#[tauri::command]
|
||||
|
|
@ -550,9 +569,15 @@ fn migrate_data_dir(
|
|||
old_dir: Option<String>,
|
||||
new_dir: String,
|
||||
strategy: String,
|
||||
state: tauri::State<'_, MigrationState>,
|
||||
) -> Result<(), String> {
|
||||
let old = docs_dir(&app, &old_dir);
|
||||
let approved_dir = state.0.lock().unwrap().take();
|
||||
let new = PathBuf::from(&new_dir);
|
||||
if Some(new.clone()) != approved_dir {
|
||||
return Err("Migration rejected: target directory was not explicitly chosen by the user.".into());
|
||||
}
|
||||
|
||||
let old = docs_dir(&app, &old_dir);
|
||||
fs::create_dir_all(&new).map_err(|e| e.to_string())?;
|
||||
// canonicalize: "C:\Data" and "c:\data" are the same folder on Windows
|
||||
let canon_old = fs::canonicalize(&old).unwrap_or_else(|_| old.clone());
|
||||
|
|
@ -743,7 +768,7 @@ fn main() {
|
|||
run_backups,
|
||||
backup_deleted_sheet,
|
||||
open_backups_folder,
|
||||
data_dir_has_documents,
|
||||
pick_data_dir,
|
||||
migrate_data_dir,
|
||||
fetch_rates,
|
||||
fetch_historical_rates,
|
||||
|
|
@ -758,6 +783,7 @@ fn main() {
|
|||
write_image_file
|
||||
])
|
||||
.setup(|app| {
|
||||
app.manage(MigrationState(Mutex::new(None)));
|
||||
let handle = app.handle().clone();
|
||||
let show = MenuItem::with_id(app, "show", "Show / Hide", true, None::<&str>)?;
|
||||
let quit = MenuItem::with_id(app, "quit", "Quit Summarum", true, None::<&str>)?;
|
||||
|
|
|
|||
10
src/main.ts
10
src/main.ts
|
|
@ -8,7 +8,7 @@ import {
|
|||
fetchRates, fetchMarketData, fetchHistoricalRates, loadExtensionScripts, openExtensionsFolder, isTauri,
|
||||
getLaunchFile, onOpenFile, onFileDrop,
|
||||
setDataDir, runBackups, backupDeletedSheet, openBackupsFolder,
|
||||
chooseFolder, dataDirHasDocuments, migrateDataDir,
|
||||
pickDataDir, migrateDataDir,
|
||||
} from "./storage";
|
||||
import type { LineResult } from "./engine";
|
||||
import { setLang, detectLang, t } from "./i18n";
|
||||
|
|
@ -404,10 +404,12 @@ function bindDataDirSettings(): void {
|
|||
};
|
||||
renderDataDir();
|
||||
dataDirBtn.addEventListener("click", async () => {
|
||||
const picked = await chooseFolder();
|
||||
if (!picked || picked === settings.dataDir) return;
|
||||
const res = await pickDataDir();
|
||||
if (!res) return;
|
||||
const { path: picked, hasDocuments } = res;
|
||||
if (picked === settings.dataDir) return;
|
||||
let strategy: "move" | "overwrite" | "use_existing" = "move";
|
||||
if (await dataDirHasDocuments(picked)) {
|
||||
if (hasDocuments) {
|
||||
const ans = await askModal(t("folderConflict"), t("useExisting"), t("replaceMine"));
|
||||
if (ans === null) return;
|
||||
strategy = ans === "a" ? "use_existing" : "overwrite";
|
||||
|
|
|
|||
|
|
@ -167,16 +167,12 @@ export async function openBackupsFolder(dataDir: string): Promise<void> {
|
|||
if (isTauri()) await invoke("open_backups_folder", { dir: dirArg(dataDir) });
|
||||
}
|
||||
|
||||
export async function chooseFolder(): Promise<string | null> {
|
||||
export async function pickDataDir(): Promise<{ path: string, hasDocuments: boolean } | null> {
|
||||
if (!isTauri()) return null;
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const picked = await open({ directory: true, multiple: false });
|
||||
return typeof picked === "string" ? picked : null;
|
||||
}
|
||||
|
||||
export async function dataDirHasDocuments(dir: string): Promise<boolean> {
|
||||
if (!isTauri()) return false;
|
||||
return invoke<boolean>("data_dir_has_documents", { dir });
|
||||
return invoke<{ path: string, has_documents: boolean } | null>("pick_data_dir").then(res => {
|
||||
if (!res) return null;
|
||||
return { path: res.path, hasDocuments: res.has_documents };
|
||||
});
|
||||
}
|
||||
|
||||
export type MigrateStrategy = "move" | "overwrite" | "use_existing";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue