From 1d7d5c08586d18d517a6686126b4e914237d6316 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:09:55 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20path=20traversal=20in=20manual=20path=20resolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes a critical path traversal bypass in `TypeScriptDependencyExtractor` caused by incorrectly popping empty/`ParentDir` path components and improperly deleting absolute path boundaries (`RootDir` and `Prefix`). This enforces standard lexical normalization semantics on the component vector stack. Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ .../src/incremental/extractors/typescript.rs | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..4de062f --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-02-28 - [Path Traversal bypass in PathBuf resolution] +**Vulnerability:** Found a vulnerable path component normalization in `crates/flow/src/incremental/extractors/typescript.rs`. Manual handling of `..` (`ParentDir`) blindly called `components.pop()`. This fails to handle root directories or prefixes correctly (converting absolute paths to relative, allowing traversal escapes), and incorrectly ignores `..` at the beginning of relative paths (e.g., resolving `../../a` to `a`). +**Learning:** `std::path::PathBuf::components()` correctly yields `Component::RootDir` and `Component::ParentDir`. Normalization algorithms MUST explicitly check the previous component before popping. Popping `RootDir` is inherently unsafe as it changes path type. Empty states or states ending in `ParentDir` must push `ParentDir` instead of doing nothing or blindly popping. +**Prevention:** Whenever manual path resolution loop involves popping on `Component::ParentDir`, check the top of the component stack first to prevent popping prefixes/roots or ignoring `ParentDir` when the stack is empty/only contains `ParentDir`. diff --git a/crates/flow/src/incremental/extractors/typescript.rs b/crates/flow/src/incremental/extractors/typescript.rs index 1bdda4e..da3ab99 100644 --- a/crates/flow/src/incremental/extractors/typescript.rs +++ b/crates/flow/src/incremental/extractors/typescript.rs @@ -808,7 +808,21 @@ impl TypeScriptDependencyExtractor { for component in resolved.components() { match component { std::path::Component::ParentDir => { - components.pop(); + let pop = match components.last() { + Some(std::path::Component::RootDir) + | Some(std::path::Component::Prefix(_)) => false, + None | Some(std::path::Component::ParentDir) => false, + _ => true, + }; + if pop { + components.pop(); + } else if !matches!( + components.last(), + Some(std::path::Component::RootDir) + | Some(std::path::Component::Prefix(_)) + ) { + components.push(component); + } } std::path::Component::CurDir => {} _ => components.push(component),