Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2024-05-20 - Pre-canonicalize base path in FileSystemContext to avoid symlink escapes
**Vulnerability:** The `FileSystemContext::new` accepted an uncanonicalized base path, which could potentially lead to symlink escapes during path validation.
**Learning:** By canonicalizing the base path during construction, the check inside `secure_path` accurately determines if the target escapes the expected path, which wasn't fully guaranteed with an uncanonicalized base path.
**Prevention:** Always construct secure contexts with canonicalized paths when implementing file path validation checks, especially to prevent symlink traversal.
Comment on lines +1 to +4
11 changes: 6 additions & 5 deletions crates/services/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,11 @@ pub struct FileSystemContext {
}

impl FileSystemContext {
pub fn new<P: AsRef<Path>>(base_path: P) -> Self {
Self {
base_path: base_path.as_ref().to_path_buf(),
}
pub fn new<P: AsRef<Path>>(base_path: P) -> ServiceResult<Self> {
let canonical_base = base_path.as_ref().canonicalize()?;
Comment on lines +145 to +146
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Canonicalizing the base path in new may reject otherwise valid configurations where the directory is created later.

This constructor now requires the directory to pre-exist, so callers can’t create a context for a path that will be created later. If that scenario should remain supported, consider either delaying canonicalization until first use, or eagerly creating the directory (e.g., via create_dir_all) before canonicalizing, depending on the intended invariants.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider enriching the error from canonicalize() with path context for easier debugging and observability.

Using ? here returns the bare canonicalize error, which can be hard to trace. Consider wrapping it (e.g. via a custom error variant or map_err that includes the base_path) so callers can see which base path failed when constructing different FileSystemContext roots.

Suggested implementation:

    pub fn new<P: AsRef<Path>>(base_path: P) -> ServiceResult<Self> {
        let canonical_base = base_path
            .as_ref()
            .canonicalize()
            .map_err(|e| {
                std::io::Error::new(
                    e.kind(),
                    format!(
                        "failed to canonicalize base path '{}': {}",
                        base_path.as_ref().display(),
                        e
                    ),
                )
            })?;
        Ok(Self {
            base_path: canonical_base,
        })
    }

If ServiceResult does not currently accept std::io::Error via From<std::io::Error>, you will need to:

  1. Ensure the error type used by ServiceResult implements From<std::io::Error> (or adjust the map_err to construct the appropriate service-specific error variant instead of std::io::Error::new).
  2. Optionally, if you already use a custom error type with richer context (e.g. via thiserror or anyhow::Context), replace the std::io::Error::new(...) construction with your existing pattern while keeping the base_path.as_ref().display() information in the message.

Ok(Self {
base_path: canonical_base,
})
}

/// Lexically validate path to prevent traversal attacks and symlink escapes
Expand Down Expand Up @@ -310,7 +311,7 @@ mod tests {
#[test]
fn test_file_system_context_security() {
let temp = std::env::temp_dir();
let ctx = FileSystemContext::new(&temp);
let ctx = FileSystemContext::new(&temp).unwrap();

// Valid paths
assert!(ctx.secure_path("test.txt").is_ok());
Expand Down
2 changes: 1 addition & 1 deletion crates/thread/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ fn test_service_reexports_work() {
use thread::services::FileSystemContext;

// Just check if we can use the types
let _ctx = FileSystemContext::new(".");
let _ctx = FileSystemContext::new(".").unwrap();
}