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 <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-09-02 15:36:26 +08:00
parent 7192f8230e
commit 09a75482ae
4 changed files with 13 additions and 12 deletions

View File

@@ -0,0 +1 @@
timestamp = 1787231066609

View File

@@ -133,11 +133,16 @@ if __name__ == '__main__':
# So we need to check if the executable is in the folder, and if so, concat again. # 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)): if os.path.exists(os.path.join(folder, options.executable)):
options.executable = 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) 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") print("The executable must locate in source folder")
exit(-1) exit(-1)
exe = '.' + exe[len(os.path.abspath(folder)):] exe = '.' + exe[len(folder_path):]
print("Executable path: " + exe) print("Executable path: " + exe)
print("Compression level: " + str(options.level)) print("Compression level: " + str(options.level))
md5_table = generate_md5_table( md5_table = generate_md5_table(

View File

@@ -90,7 +90,7 @@ fn merge(
(files, exe) (files, exe)
} }
fn normalize_path(path: &str) -> String { pub(crate) fn normalize_path(path: &str) -> String {
path.replace('\\', "/") path.replace('\\', "/")
.trim_start_matches("./") .trim_start_matches("./")
.to_lowercase() .to_lowercase()

View File

@@ -5,7 +5,7 @@ use std::{
process::{Command, Stdio}, process::{Command, Stdio},
}; };
use bin_reader::BinaryReader; use bin_reader::{normalize_path, BinaryReader};
pub mod bin_reader; pub mod bin_reader;
#[cfg(windows)] #[cfg(windows)]
@@ -84,12 +84,6 @@ fn previous_package_files(dir: &Path) -> Vec<String> {
.collect() .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, // 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 // 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: // prefix, root or parent component would otherwise escape the extraction directory:
@@ -133,10 +127,11 @@ fn remove_dropped_package_files_with<F>(
where where
F: FnMut(&Path) -> std::io::Result<()>, F: FnMut(&Path) -> std::io::Result<()>,
{ {
let keep: std::collections::HashSet<String> = current.iter().map(|p| normalized(p)).collect(); let keep: std::collections::HashSet<String> =
current.iter().map(|p| normalize_path(p)).collect();
let mut failed = Vec::new(); let mut failed = Vec::new();
for previous in previous_package_files(dir) { for previous in previous_package_files(dir) {
if keep.contains(&normalized(&previous)) { if keep.contains(&normalize_path(&previous)) {
continue; continue;
} }
let Some(path) = resolve_within(dir, &previous) else { let Some(path) = resolve_within(dir, &previous) else {