fix(LCEL-01): disable arbitrary-path IPC + remove opener ** scope

three changes:

1. file_dialogs::write_binary_file / read_binary_file used to accept
   arbitrary paths with no validation, no dialog, no scope. now they
   refuse all paths. callers must use pick_file / save_file_dialog.

2. plugins::list_directory used to accept arbitrary paths with no
   validation. now it refuses all paths. callers must use
   get_plugins_dir.

3. capabilities/default.json had opener:allow-open-path scoped to
   path: "**" which let the webview launch ANY local path. removed
   that entry entirely. opener:allow-reveal-item-in-dir is left for
   now (used by the file manager UI).

combined, these close the webview -> arbitrary write+execute primitive.
the proper fix is to scope opener:allow-open-path to a specific games
directory once we know what paths the launcher actually needs to open.
This commit is contained in:
M1noa 2026-07-16 17:41:38 -05:00
parent 8da6a217bc
commit 8c941a6b72
3 changed files with 26 additions and 22 deletions

View file

@ -23,14 +23,13 @@
"gamepad:default",
"drpc:default",
"updater:default",
{
"identifier": "opener:allow-open-path",
"allow": [
{
"path": "**"
}
]
},
// security: removed opener:allow-open-path with path: "**" - that
// scoped the webview to launch ANY local path. combined with
// write_binary_file + list_directory (now disabled, see file_dialogs.rs
// and plugins.rs) a compromised webview had a full RCE primitive.
// (LCEL-01)
// TODO: scope to a specific games directory (e.g. the instance dir)
// once we know what paths the launcher actually needs to open.
{
"identifier": "opener:allow-reveal-item-in-dir",
"allow": [

View file

@ -40,12 +40,20 @@ pub fn save_file_dialog(title: String, filename: String, filters: Vec<String>) -
}
}
// security: write_binary_file / read_binary_file used to accept arbitrary
// paths with no validation, no dialog, no scope. combined with the
// opener:allow-open-path: ** capability, a compromised webview could
// write+launch any file anywhere. (LCEL-01)
// TODO: scope these to a launcher-specific subdirectory with canonical
// containment checks, or remove from the IPC surface entirely and route
// all binary file ops through rfd-gated dialogs.
// for now, refuse all paths. callers must use pick_file / save_file_dialog.
#[tauri::command]
pub fn write_binary_file(path: String, data: Vec<u8>) -> Result<(), String> {
fs::write(path, data).map_err(|e| e.to_string())
pub fn write_binary_file(_path: String, _data: Vec<u8>) -> Result<(), String> {
Err("write_binary_file disabled: use save_file_dialog instead".into())
}
#[tauri::command]
pub fn read_binary_file(path: String) -> Result<Vec<u8>, String> {
fs::read(path).map_err(|e| e.to_string())
pub fn read_binary_file(_path: String) -> Result<Vec<u8>, String> {
Err("read_binary_file disabled: use pick_file instead".into())
}

View file

@ -15,16 +15,13 @@ pub fn get_plugins_dir(app: tauri::AppHandle) -> Result<String, String> {
#[tauri::command]
pub fn list_directory(path: String) -> Result<Vec<DirEntry>, String> {
let entries = fs::read_dir(&path).map_err(|e| e.to_string())?;
let mut results = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| e.to_string())?;
results.push(DirEntry {
name: entry.file_name().to_string_lossy().to_string(),
is_dir: entry.file_type().map(|t| t.is_dir()).unwrap_or(false),
});
}
Ok(results)
// security: list_directory used to accept arbitrary paths with no
// validation. combined with write_binary_file + opener:allow-open-path: **
// a compromised webview could enumerate startup folders, drop a binary,
// and launch it. (LCEL-01)
// TODO: scope to the launcher's plugins dir or remove from IPC entirely.
// for now, refuse all paths.
Err("list_directory disabled: use get_plugins_dir instead".into())
}
#[tauri::command]