From b495e538c85e969f90072cc682d43b1e520fafde Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:38:46 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Harden=20mo?= =?UTF-8?q?d=20name=20and=20version=20validation=20against=20path=20traver?= =?UTF-8?q?sal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: TheDragonSoft <75906592+TheDragonSoft@users.noreply.github.com> --- src-tauri/src/core/services/downloader.rs | 22 +++++++++++++++++--- src-tauri/src/core/services/portal_client.rs | 14 ++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/core/services/downloader.rs b/src-tauri/src/core/services/downloader.rs index fc8b739..5ccd53b 100644 --- a/src-tauri/src/core/services/downloader.rs +++ b/src-tauri/src/core/services/downloader.rs @@ -611,9 +611,12 @@ fn remove_other_versions(dir: &Path, keep: &str, mod_name: &str) -> std::io::Res } pub(crate) fn plausible_version(s: &str) -> bool { - !s.is_empty() - && s.chars() - .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_')) + let trimmed = s.trim(); + !trimmed.is_empty() + && !trimmed.contains("..") + && !trimmed.starts_with('.') + && !trimmed.ends_with('.') + && trimmed.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_')) } fn anticache_token() -> String { @@ -826,6 +829,19 @@ mod tests { .expect("should match actual info.json, ignoring docs/extra_info.json"); } + #[test] + fn version_validation() { + assert!(!plausible_version("")); + assert!(!plausible_version(" ")); + assert!(!plausible_version("..")); + assert!(!plausible_version(".1.0")); + assert!(!plausible_version("1.0.")); + assert!(!plausible_version("1..0")); + assert!(plausible_version("1.0.0")); + assert!(plausible_version("0.18.2")); + assert!(plausible_version("2.1.2-alpha")); + } + #[test] fn download_queue_tracks_in_flight_and_active_jobs() { let queue = DownloadQueue::new(reqwest::Client::new()); diff --git a/src-tauri/src/core/services/portal_client.rs b/src-tauri/src/core/services/portal_client.rs index 900be2e..a135662 100644 --- a/src-tauri/src/core/services/portal_client.rs +++ b/src-tauri/src/core/services/portal_client.rs @@ -261,10 +261,14 @@ fn map_release(r: PortalRelease) -> ModRelease { } /// Portal names are [A-Za-z0-9 _.-] in practice (spaces occur, e.g. "Flow Control"). +/// Reject empty strings, strings containing ".." (path traversal), or leading/trailing dots. pub fn plausible_name(s: &str) -> bool { - !s.trim().is_empty() - && s.chars() - .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | ' ')) + let trimmed = s.trim(); + !trimmed.is_empty() + && !trimmed.contains("..") + && !trimmed.starts_with('.') + && !trimmed.ends_with('.') + && trimmed.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | ' ')) } /// Percent-encode anything outside the URL-safe set (names may contain spaces). @@ -797,6 +801,10 @@ mod tests { assert!(!plausible_name("")); assert!(!plausible_name(" ")); assert!(!plausible_name("../etc")); + assert!(!plausible_name("..")); + assert!(!plausible_name(".mod")); + assert!(!plausible_name("mod.")); + assert!(!plausible_name("a..b")); assert!(!plausible_name("+ ChangeInserterDropLane")); assert!(plausible_name("even-distribution")); assert!(plausible_name("Flow Control"));