From 6ad86f6148d5edf683171128cf0a940a5e22145c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:48:03 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=94=92=20prevent=20path=20traversa?= =?UTF-8?q?l=20in=20FileSystemContext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement lexical path validation in `FileSystemContext` to prevent unauthorized file access and overwrites outside the base directory. Add unit tests to verify the fix against absolute paths and traversal attacks. Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com> --- crates/services/src/lib.rs | 58 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/services/src/lib.rs b/crates/services/src/lib.rs index c932db7..272f572 100644 --- a/crates/services/src/lib.rs +++ b/crates/services/src/lib.rs @@ -147,16 +147,48 @@ impl FileSystemContext { base_path: base_path.as_ref().to_path_buf(), } } + + /// Lexically validate path to prevent traversal attacks + fn secure_path(&self, source: &str) -> Result { + use std::path::Component; + + let path = Path::new(source); + let mut depth = 0; + + for component in path.components() { + match component { + Component::Prefix(_) | Component::RootDir => { + return Err(ServiceError::execution_dynamic(format!( + "Absolute paths are not allowed: {source}" + ))); + } + Component::CurDir => {} + Component::ParentDir => { + if depth == 0 { + return Err(ServiceError::execution_dynamic(format!( + "Path traversal outside of base path is not allowed: {source}" + ))); + } + depth -= 1; + } + Component::Normal(_) => { + depth += 1; + } + } + } + + Ok(self.base_path.join(source)) + } } impl ExecutionContext for FileSystemContext { fn read_content(&self, source: &str) -> Result { - let path = self.base_path.join(source); + let path = self.secure_path(source)?; Ok(std::fs::read_to_string(path)?) } fn write_content(&self, destination: &str, content: &str) -> Result<(), ServiceError> { - let path = self.base_path.join(destination); + let path = self.secure_path(destination)?; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } @@ -235,4 +267,26 @@ mod tests { let sources = ctx.list_sources().unwrap(); assert_eq!(sources, vec!["test.rs"]); } + + #[test] + fn test_file_system_context_security() { + let temp = std::env::temp_dir(); + let ctx = FileSystemContext::new(&temp); + + // Valid paths + assert!(ctx.secure_path("test.txt").is_ok()); + assert!(ctx.secure_path("dir/test.txt").is_ok()); + assert!(ctx.secure_path("./test.txt").is_ok()); + assert!(ctx.secure_path("dir/../test.txt").is_ok()); + + // Absolute paths + assert!(ctx.secure_path("/etc/passwd").is_err()); + #[cfg(windows)] + assert!(ctx.secure_path("C:\\Windows\\System32\\cmd.exe").is_err()); + + // Traversal attacks + assert!(ctx.secure_path("../test.txt").is_err()); + assert!(ctx.secure_path("dir/../../test.txt").is_err()); + assert!(ctx.secure_path("dir/../inc/../../test.txt").is_err()); + } }