diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..f81a465a --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,5 @@ + +## 2024-05-25 - Path Traversal in TypeScript Extractor +**Vulnerability:** The manual path resolution logic in `crates/flow/src/incremental/extractors/typescript.rs` naively popped the last component when encountering `..` (`std::path::Component::ParentDir`). This allowed escaping the root directory by popping `RootDir` or `Prefix`, and failed to handle relative paths like `../../a` correctly. +**Learning:** `std::path::Path::components()` and `Vec::pop()` are not sufficient for secure path normalization. Popping `RootDir` effectively makes an absolute path relative, and popping an empty list discards `..` segments that might be necessary for relative traversal. +**Prevention:** Always check `components.last()` before popping. Prevent popping if the last component is `RootDir` or `Prefix`. If the list is empty or the last component is already `ParentDir`, push the new `ParentDir` to preserve correct relative path structure. diff --git a/crates/flow/src/incremental/extractors/typescript.rs b/crates/flow/src/incremental/extractors/typescript.rs index 1bdda4ef..95898957 100644 --- a/crates/flow/src/incremental/extractors/typescript.rs +++ b/crates/flow/src/incremental/extractors/typescript.rs @@ -808,7 +808,20 @@ impl TypeScriptDependencyExtractor { for component in resolved.components() { match component { std::path::Component::ParentDir => { - components.pop(); + if let Some(last) = components.last() { + if matches!( + last, + std::path::Component::RootDir | std::path::Component::Prefix(_) + ) { + // Security: prevent popping root or prefix + } else if matches!(last, std::path::Component::ParentDir) { + components.push(component); + } else { + components.pop(); + } + } else { + components.push(component); + } } std::path::Component::CurDir => {} _ => components.push(component),