🛡️ Sentinel: [CRITICAL] Fix Path Traversal in FileSystemContext#169
🛡️ Sentinel: [CRITICAL] Fix Path Traversal in FileSystemContext#169bashandbone wants to merge 1 commit intomainfrom
Conversation
Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
Reviewer's GuideCanonicalizes FileSystemContext base paths at construction time, turns the constructor into a fallible Result-based API, and simplifies secure_path’s physical symlink check to rely on the pre-canonicalized base path, updating tests and adding a Sentinel note for the vulnerability and fix. Sequence diagram for constructing FileSystemContext with pre-canonicalized base pathsequenceDiagram
actor Caller
participant FileSystemContext
participant OS as FileSystem
participant ServiceError
Caller->>FileSystemContext: new(base_path)
activate FileSystemContext
FileSystemContext->>OS: canonicalize(base_path)
alt canonicalization succeeds
OS-->>FileSystemContext: Ok(canonical_base)
FileSystemContext-->>Caller: Ok(FileSystemContext { base_path: canonical_base })
else canonicalization fails
OS-->>FileSystemContext: Err(io_error)
FileSystemContext->>ServiceError: execution_dynamic("Failed to canonicalize base path: " + io_error)
ServiceError-->>FileSystemContext: ServiceError
FileSystemContext-->>Caller: Err(ServiceError)
end
deactivate FileSystemContext
Class diagram for updated FileSystemContext construction and security checksclassDiagram
class FileSystemContext {
+PathBuf base_path
+new(base_path: P) ServiceResult_FileSystemContext
+secure_path(source: &str) ServiceResult_PathBuf
}
class ServiceResult_FileSystemContext {
}
class ServiceResult_PathBuf {
}
class ServiceError {
+execution_dynamic(message: String) ServiceError
}
class PathBuf {
}
FileSystemContext ..> PathBuf : uses
FileSystemContext ..> ServiceResult_FileSystemContext : returns
FileSystemContext ..> ServiceResult_PathBuf : returns
ServiceResult_FileSystemContext ..> ServiceError : error
ServiceResult_PathBuf ..> ServiceError : error
%% Rust generic aliasing represented as separate pseudo-types
ServiceResult_FileSystemContext <|-- ServiceResult_PathBuf
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Pull request overview
This PR hardens FileSystemContext against a path traversal / symlink-escape bypass by ensuring the sandbox base path is canonicalized once at initialization, eliminating a runtime failure mode where the physical symlink check could be skipped.
Changes:
- Change
FileSystemContext::newto returnServiceResult<Self>and store a canonicalizedbase_path. - Remove redundant
base_path.canonicalize()fromsecure_pathand rely on the pre-canonicalized base. - Update affected tests/call sites to handle the new
Resultreturn type.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| crates/services/src/lib.rs | Canonicalize base_path in constructor; adjust symlink-escape check to use stored canonical base; update unit test construction. |
| crates/thread/tests/integration.rs | Update integration test to unwrap the new Result returned by FileSystemContext::new. |
| .jules/sentinel.md | Add a Sentinel note documenting the vulnerability and the preventative pattern. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pub fn new<P: AsRef<Path>>(base_path: P) -> ServiceResult<Self> { | ||
| let canonical_base = base_path.as_ref().canonicalize().map_err(|e| { | ||
| ServiceError::execution_dynamic(format!("Failed to canonicalize base path: {}", e)) | ||
| })?; | ||
| Ok(Self { | ||
| base_path: canonical_base, | ||
| }) |
There was a problem hiding this comment.
FileSystemContext::new maps canonicalization failures to ServiceError::execution_dynamic(...), but failing to canonicalize the base path is a configuration/initialization problem rather than an execution-context failure. Consider using ServiceError::config_dynamic(...) (and include the provided base_path in the message) so callers can distinguish misconfiguration from runtime execution errors and get better diagnostics.
| if current.exists() | ||
| && let Ok(canonical_prefix) = current.canonicalize() | ||
| && !canonical_prefix.starts_with(&self.base_path) | ||
| { | ||
| return Err(ServiceError::execution_dynamic(format!( | ||
| "Path validation failed: {source} resolves outside base path via symlinks" | ||
| ))); |
There was a problem hiding this comment.
The physical symlink-escape check silently skips validation when current.canonicalize() fails (let Ok(canonical_prefix) = ...). For a security boundary, this should fail closed; otherwise, a permission/IO error during canonicalization can bypass the symlink-escape check. Consider returning an error when current.exists() but canonicalize() returns Err, instead of treating it as a pass.
| if current.exists() | |
| && let Ok(canonical_prefix) = current.canonicalize() | |
| && !canonical_prefix.starts_with(&self.base_path) | |
| { | |
| return Err(ServiceError::execution_dynamic(format!( | |
| "Path validation failed: {source} resolves outside base path via symlinks" | |
| ))); | |
| if current.exists() { | |
| let canonical_prefix = current.canonicalize().map_err(|e| { | |
| ServiceError::execution_dynamic(format!( | |
| "Path validation failed: could not canonicalize existing path prefix for {source}: {e}" | |
| )) | |
| })?; | |
| if !canonical_prefix.starts_with(&self.base_path) { | |
| return Err(ServiceError::execution_dynamic(format!( | |
| "Path validation failed: {source} resolves outside base path via symlinks" | |
| ))); | |
| } |
| fn test_file_system_context_security() { | ||
| let temp = std::env::temp_dir(); | ||
| let ctx = FileSystemContext::new(&temp); | ||
| let ctx = FileSystemContext::new(&temp).unwrap(); | ||
|
|
There was a problem hiding this comment.
The unit tests exercise lexical traversal cases, but they don't cover the symlink-escape scenario this change is addressing (e.g., base path provided via symlink that is swapped after context creation, or a symlink inside the sandbox pointing outside). Adding a regression test here would help prevent reintroducing the bypass and validate the physical check behavior.
🚨 Severity: CRITICAL
💡 Vulnerability: The
FileSystemContext::secure_pathmethod contained a path traversal bypass vulnerability. The physical security check designed to prevent symlink escapes was enclosed within anif let Ok(canonical_base) = self.base_path.canonicalize()block. If canonicalization of the base path failed at the time of access (e.g., due to permission changes or if the base directory was moved/deleted), the physical security check was silently skipped, potentially allowing an attacker to escape the sandbox via malicious symlinks.🎯 Impact: An attacker could bypass directory sandbox restrictions and read or write files outside the intended base path via path traversal attacks.
🔧 Fix: Modified
FileSystemContext::newto canonicalize thebase_pathupon initialization and return aServiceResult<Self>. This ensures that a valid, canonicalized base path is always established before any operations occur. The redundant canonicalization insecure_pathwas removed, preventing the silent bypass. Test cases were updated to handle the newResultreturn type.✅ Verification: Ran unit tests in
thread-services(cargo test -p thread-services) and integration tests inthread(cargo test -p thread), ensuring the fix did not introduce regressions and that the path traversal protections function correctly.PR created automatically by Jules for task 1793067458857301517 started by @bashandbone
Summary by Sourcery
Enforce canonicalization of FileSystemContext base paths at initialization to close a path traversal/symlink escape vulnerability and update tests and internal security notes accordingly.
Bug Fixes:
Enhancements:
Documentation:
Tests:
Chores: