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 @@
## 2025-05-15 - [Path Traversal in Manual Path Normalization]
**Vulnerability:** Manual path resolution using `components.pop()` on `std::path::Component::ParentDir` allowed path traversal. If a path like `../../a` was parsed, `components.pop()` on an empty `Vec` did nothing, turning the path into `a` instead of preserving the parent traversal. It could also pop `RootDir` or `Prefix` components, changing absolute paths to relative ones or traversing beyond intended roots.
**Learning:** `std::path::Component` normalization must handle empty lists and consecutive `ParentDir` components by pushing them instead of ignoring them. It must also explicitly avoid popping `RootDir` or `Prefix` components to prevent escaping virtual file systems or simulated root directories.
**Prevention:** Explicitly check if the `components` list is empty, if the last component is `ParentDir`, or if the last component is `RootDir` / `Prefix` before calling `pop()`.
Comment on lines +1 to +4
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

New markdown files in this repo typically include an SPDX header (often via an HTML comment block) or are covered by a REUSE.toml annotation. This file currently has no SPDX metadata and appears not to be covered by the existing REUSE.toml annotations, which can cause the fsfe/reuse-action CI job to fail. Add an SPDX header to this file or extend REUSE.toml to cover .jules/**.

Copilot uses AI. Check for mistakes.
13 changes: 12 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,18 @@ impl TypeScriptDependencyExtractor {
for component in resolved.components() {
match component {
std::path::Component::ParentDir => {
components.pop();
let is_empty = components.is_empty();
let last_is_parent = matches!(components.last(), Some(std::path::Component::ParentDir));
let last_is_root_or_prefix = matches!(
components.last(),
Some(std::path::Component::RootDir) | Some(std::path::Component::Prefix(_))
Comment on lines +812 to +815
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

components.last() returns Option<&std::path::Component>, but the matches! patterns here are written as if they were matching Option<std::path::Component>. This is likely a compile error (and will also be caught by clippy). Update the patterns to match references (e.g., Some(&Component::ParentDir) / Some(&Component::RootDir) / Some(&Component::Prefix(_))) or bind components.last() to a variable and match on *last/last appropriately.

Suggested change
let last_is_parent = matches!(components.last(), Some(std::path::Component::ParentDir));
let last_is_root_or_prefix = matches!(
components.last(),
Some(std::path::Component::RootDir) | Some(std::path::Component::Prefix(_))
let last_is_parent =
matches!(components.last(), Some(&std::path::Component::ParentDir));
let last_is_root_or_prefix = matches!(
components.last(),
Some(&std::path::Component::RootDir)
| Some(&std::path::Component::Prefix(_))

Copilot uses AI. Check for mistakes.
);

if is_empty || last_is_parent {
components.push(component);
} else if !last_is_root_or_prefix {
components.pop();
}
Comment on lines +818 to +822
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

This change fixes tricky .. normalization edge cases, but there are no tests covering the newly handled scenarios (leading ../../... that should remain relative, and .. following RootDir/Prefix that must not pop them). Please add unit tests exercising these cases so the security fix can’t regress silently; the existing resolve_module_path tests in crates/flow/tests/extractor_typescript_tests.rs can be extended with a couple of dedicated cases.

Copilot uses AI. Check for mistakes.
}
std::path::Component::CurDir => {}
_ => components.push(component),
Expand Down
Loading