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 @@
## 2026-05-31 - [Path Traversal in Manual Path Resolution]
**Vulnerability:** The manual path resolution logic in the TypeScript extractor popped path components directly when encountering `Component::ParentDir`. This could allow traversal escapes if paths like `../../foo` were evaluated, as the `ParentDir` components were erroneously dropped when the components stack was empty.
**Learning:** Rust's `PathBuf::canonicalize()` resolves `..`, but when dealing with unresolved paths (e.g., inside non-existent directories), manual component traversal must handle `ParentDir` correctly. Specifically, dropping `ParentDir` without checking if the base of the path is already relative can truncate directory traversal semantics inappropriately.
**Prevention:** When manually resolving paths with `std::path::Component`, explicitly block `Component::ParentDir` from popping `Component::RootDir` or `Component::Prefix`. Furthermore, if the current component list is empty or its last element is already `Component::ParentDir`, append the `Component::ParentDir` instead of ignoring it.
14 changes: 13 additions & 1 deletion crates/flow/src/incremental/extractors/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,19 @@ impl TypeScriptDependencyExtractor {
for component in resolved.components() {
match component {
std::path::Component::ParentDir => {
components.pop();
let last = components.last();
match last {
Some(std::path::Component::RootDir)
| Some(std::path::Component::Prefix(_)) => {
// Do not pop RootDir or Prefix to prevent traversal escapes
}
Some(std::path::Component::ParentDir) | None => {
Comment on lines +811 to +817

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.

🚨 question (security): Re-check intended behavior for leading .. components when normalizing paths.

Because pop() on an empty components was a no-op, leading ParentDir segments were previously discarded. Now, when last is None, they are preserved (e.g. ../foo remains ../foo). If this function is used as part of a security boundary (e.g., to enforce confinement under a root), preserving leading .. could reintroduce traversal concerns. Please confirm whether callers rely on this to yield a path guaranteed under a root, or whether leading .. is expected and validated elsewhere.

components.push(component);
}
_ => {
components.pop();
}
}
Comment on lines 810 to +823
}
std::path::Component::CurDir => {}
_ => components.push(component),
Expand Down