diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..9f076026 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2024-05-30 - Path Traversal in Manual Path Normalization +**Vulnerability:** Path traversal vulnerability in TypeScript module resolution where manual `../` (ParentDir) handling in `typescript.rs` popped components indiscriminately, even after exhausting the base path. +**Learning:** When falling back to manual path normalisation via `.components()` because a file doesn't exist yet (canonicalize fails), naively popping components on `Component::ParentDir` allows malicious inputs like `../../` to escape the root directory context or resolve to arbitrary files, completely bypassing intended base path restrictions. +**Prevention:** Always preserve `ParentDir` components when the component list is empty or already ends in `ParentDir`. Never pop `RootDir` or `Prefix` components, effectively treating the root as an absolute boundary that `..` cannot traverse above. diff --git a/crates/flow/src/incremental/extractors/typescript.rs b/crates/flow/src/incremental/extractors/typescript.rs index 1bdda4ef..227229d5 100644 --- a/crates/flow/src/incremental/extractors/typescript.rs +++ b/crates/flow/src/incremental/extractors/typescript.rs @@ -808,7 +808,16 @@ impl TypeScriptDependencyExtractor { for component in resolved.components() { match component { std::path::Component::ParentDir => { - components.pop(); + if components.is_empty() || components.last() == Some(&std::path::Component::ParentDir) { + components.push(std::path::Component::ParentDir); + } else if let Some(last) = components.last() { + match last { + std::path::Component::RootDir | std::path::Component::Prefix(_) => { + // Cannot go above root/prefix, ignore + } + _ => { components.pop(); } + } + } } std::path::Component::CurDir => {} _ => components.push(component),