diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..8c67394 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-02-27 - [CRITICAL] Fix Path Traversal in TypeScript Module Path Resolution +**Vulnerability:** The manual path resolution fallback in `TypeScriptDependencyExtractor` for canonicalization failures allowed dropping of root prefixes (`/`, `C:\`) and silent loss of excess `..` components when normalizing path traversals. +**Learning:** This occurred because the normalization loops simply popped elements off the `std::path::Component` vector when hitting `ParentDir`, blindly removing roots or emptying without correctly appending un-resolvable `ParentDir`s to represent true relative structures. +**Prevention:** Always maintain bound safety when manually normalizing paths using `std::path::Components`. Check explicitly for `Component::RootDir` and `Component::Prefix` when encountering a `ParentDir` to stop from erasing anchors. Similarly, if the component vector ends with a `ParentDir` or is empty, a new `ParentDir` must be explicitly pushed so relative structures don't break. diff --git a/crates/flow/src/incremental/extractors/typescript.rs b/crates/flow/src/incremental/extractors/typescript.rs index 1bdda4e..6643fc4 100644 --- a/crates/flow/src/incremental/extractors/typescript.rs +++ b/crates/flow/src/incremental/extractors/typescript.rs @@ -804,11 +804,28 @@ impl TypeScriptDependencyExtractor { resolved = canonical; } else { // If canonicalize fails (file doesn't exist), manually resolve - let mut components = Vec::new(); + let mut components: Vec = Vec::new(); for component in resolved.components() { match component { std::path::Component::ParentDir => { - components.pop(); + // Security fix: prevent path traversal attacks by explicitly blocking Component::ParentDir + // from popping Component::RootDir or Component::Prefix. + // If empty or already ends in ParentDir, push to preserve relative paths like ../../a + if let Some(last) = components.last() { + match last { + std::path::Component::RootDir | std::path::Component::Prefix(_) => { + // Cannot go above root/prefix, ignore ParentDir + } + std::path::Component::ParentDir => { + components.push(component); + } + _ => { + components.pop(); + } + } + } else { + components.push(component); + } } std::path::Component::CurDir => {} _ => components.push(component),