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-06-01 - Path Traversal Vulnerability in TypeScript Import Resolution
**Vulnerability:** A path traversal vulnerability existed in the manual path normalization logic (`resolved.components()`) inside the TypeScript extractor's `resolve_import_path` when `canonicalize` fails. The code un-conditionally popped the last path component when encountering `std::path::Component::ParentDir`, which could erroneously pop `RootDir` (`/`) or `Prefix` (`C:\`), or incorrectly remove a legitimate preceding `../` in paths like `../../a`.
**Learning:** This occurred because developers often assume `.pop()` on a vector of path components is safe for `..` without considering absolute roots, Windows prefixes, or multiple consecutive `..` components that should be preserved.
**Prevention:** Explicitly match against the last component before popping. Block `Component::ParentDir` from popping `Component::RootDir` or `Component::Prefix`, and instead push `Component::ParentDir` if the components list is empty or its last element is also `Component::ParentDir`.
10 changes: 6 additions & 4 deletions crates/ast-engine/src/tree_sitter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,9 +553,8 @@ impl ContentExt for String {
let mut bytes = std::mem::take(self).into_bytes();
let original_len = bytes.len();
bytes.splice(safe_start..safe_end, full_inserted);
*self = Self::from_utf8(bytes).unwrap_or_else(|e| {
Self::from_utf8_lossy(&e.into_bytes()).into_owned()
});
*self = Self::from_utf8(bytes)
.unwrap_or_else(|e| Self::from_utf8_lossy(&e.into_bytes()).into_owned());

// We calculate new_end_byte using the difference in the new overall string length
// to correctly align the end offset, taking any potential replacement bytes from
Expand Down Expand Up @@ -791,7 +790,10 @@ mod test {

let tree2 = parse_lang(|p| p.parse(&src, Some(&tree)), &Tsx.get_ts_language())?;
let fresh_tree = parse(&src)?;
assert_eq!(tree2.root_node().to_sexp(), fresh_tree.root_node().to_sexp());
assert_eq!(
tree2.root_node().to_sexp(),
fresh_tree.root_node().to_sexp()
);
Ok(())
}
}
18 changes: 17 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,23 @@ impl TypeScriptDependencyExtractor {
for component in resolved.components() {
match component {
std::path::Component::ParentDir => {
components.pop();
// πŸ›‘οΈ SECURITY: Prevent path traversal by explicitly blocking Component::ParentDir
// from popping RootDir/Prefix. Also correctly handle when the component list
// is empty or already ends with ParentDir to preserve paths like ../../a
if let Some(last) = components.last() {
match last {
std::path::Component::Normal(_) => {
components.pop();
}
std::path::Component::ParentDir => {
components.push(component);
}
// Don't pop RootDir or Prefix
_ => {}
}
} else {
components.push(component);
}
Comment on lines +811 to +827
}
std::path::Component::CurDir => {}
_ => components.push(component),
Expand Down
6 changes: 5 additions & 1 deletion crates/rule-engine/src/rule/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,11 @@ impl Rule {

pub fn defined_vars(&self) -> RapidSet<String> {
match self {
Rule::Pattern(p) => p.defined_vars().into_iter().map(|s| s.to_string()).collect(),
Rule::Pattern(p) => p
.defined_vars()
.into_iter()
.map(|s| s.to_string())
.collect(),
Rule::Kind(_) => RapidSet::default(),
Rule::Regex(_) => RapidSet::default(),
Rule::NthChild(n) => n.defined_vars(),
Expand Down
5 changes: 1 addition & 4 deletions crates/rule-engine/src/rule/referent_rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ impl<R> Clone for Registration<R> {

impl<R> Registration<R> {
fn read(&self) -> Arc<RapidMap<String, R>> {
self.0
.read()
.unwrap_or_else(|e| e.into_inner())
.clone()
self.0.read().unwrap_or_else(|e| e.into_inner()).clone()
}
pub(crate) fn contains_key(&self, key: &str) -> bool {
self.read().contains_key(key)
Expand Down