From 09a75482aec862a1da3fdf257abe6bfce28c76e4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 2 Sep 2026 15:36:26 +0800 Subject: [PATCH] fix(portable): validate executable path boundaries Reject executables outside the source folder and reuse the package path normalization logic during stale file cleanup. Signed-off-by: fufesou --- libs/portable/app_metadata.toml | 1 + libs/portable/generate.py | 9 +++++++-- libs/portable/src/bin_reader.rs | 2 +- libs/portable/src/main.rs | 13 ++++--------- 4 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 libs/portable/app_metadata.toml diff --git a/libs/portable/app_metadata.toml b/libs/portable/app_metadata.toml new file mode 100644 index 000000000..0aa5229dd --- /dev/null +++ b/libs/portable/app_metadata.toml @@ -0,0 +1 @@ +timestamp = 1787231066609 diff --git a/libs/portable/generate.py b/libs/portable/generate.py index 6f2471bd9..26d0b3779 100755 --- a/libs/portable/generate.py +++ b/libs/portable/generate.py @@ -133,11 +133,16 @@ if __name__ == '__main__': # So we need to check if the executable is in the folder, and if so, concat again. if os.path.exists(os.path.join(folder, options.executable)): options.executable = os.path.join(folder, options.executable) + folder_path = os.path.abspath(folder) exe: str = os.path.abspath(options.executable) - if not exe.startswith(os.path.abspath(folder)): + try: + in_source_folder = os.path.commonpath([folder_path, exe]) == folder_path + except ValueError: + in_source_folder = False + if not in_source_folder: print("The executable must locate in source folder") exit(-1) - exe = '.' + exe[len(os.path.abspath(folder)):] + exe = '.' + exe[len(folder_path):] print("Executable path: " + exe) print("Compression level: " + str(options.level)) md5_table = generate_md5_table( diff --git a/libs/portable/src/bin_reader.rs b/libs/portable/src/bin_reader.rs index c6fc42962..d488e0d1d 100644 --- a/libs/portable/src/bin_reader.rs +++ b/libs/portable/src/bin_reader.rs @@ -90,7 +90,7 @@ fn merge( (files, exe) } -fn normalize_path(path: &str) -> String { +pub(crate) fn normalize_path(path: &str) -> String { path.replace('\\', "/") .trim_start_matches("./") .to_lowercase() diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs index 56a6ee36f..284dd5ee2 100644 --- a/libs/portable/src/main.rs +++ b/libs/portable/src/main.rs @@ -5,7 +5,7 @@ use std::{ process::{Command, Stdio}, }; -use bin_reader::BinaryReader; +use bin_reader::{normalize_path, BinaryReader}; pub mod bin_reader; #[cfg(windows)] @@ -84,12 +84,6 @@ fn previous_package_files(dir: &Path) -> Vec { .collect() } -fn normalized(path: &str) -> String { - path.replace('\\', "/") - .trim_start_matches("./") - .to_lowercase() -} - // meta.toml is plain text in a user-writable directory, and it now drives deletion, // so the path is rebuilt from plain components rather than joined as written. A // prefix, root or parent component would otherwise escape the extraction directory: @@ -133,10 +127,11 @@ fn remove_dropped_package_files_with( where F: FnMut(&Path) -> std::io::Result<()>, { - let keep: std::collections::HashSet = current.iter().map(|p| normalized(p)).collect(); + let keep: std::collections::HashSet = + current.iter().map(|p| normalize_path(p)).collect(); let mut failed = Vec::new(); for previous in previous_package_files(dir) { - if keep.contains(&normalized(&previous)) { + if keep.contains(&normalize_path(&previous)) { continue; } let Some(path) = resolve_within(dir, &previous) else {