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-02-18 - Prevent Path Traversal by blocking parent popping of prefix/root in path component manipulation
**Vulnerability:** In `crates/flow/src/incremental/extractors/typescript.rs` and potentially elsewhere, path normalization popped standard components blindly when encountering `Component::ParentDir`. This could allow relative paths to evaluate to dangerous arbitrary paths.
**Learning:** Path normalization routines built on top of `std::path::Component` iterations must verify the previous component before popping it for a parent directory escape. `Component::ParentDir` should push to the component stack if the stack is empty, only has another `ParentDir`, or if the last element is `Prefix` or `RootDir` it should be ignored.
**Prevention:** In loops managing components, match on `components.last()` during a `ParentDir` to ensure we do not `pop` root directories or prefixes.
Comment on lines +1 to +4
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(())
}
}
15 changes: 11 additions & 4 deletions crates/flow/src/incremental/extractors/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -804,12 +804,19 @@ impl TypeScriptDependencyExtractor {
resolved = canonical;
} else {
// If canonicalize fails (file doesn't exist), manually resolve
let mut components = Vec::new();
let mut components: Vec<std::path::Component> = Vec::new();
for component in resolved.components() {
match component {
std::path::Component::ParentDir => {
components.pop();
}
std::path::Component::ParentDir => match components.last() {
Some(std::path::Component::Prefix(_))
| Some(std::path::Component::RootDir) => {}
Some(std::path::Component::ParentDir) | None => {
components.push(component);
}
_ => {
components.pop();
}
},
std::path::Component::CurDir => {}
_ => components.push(component),
}
Expand Down
8 changes: 4 additions & 4 deletions crates/rule-engine/src/check_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ fn get_vars_from_rules<'r>(rule: &'r Rule, utils: &'r RuleRegistration) -> Rapid
vars
}

fn check_var_in_constraints<'r>(
fn check_var_in_constraints(
mut vars: RapidSet<String>,
constraints: &'r RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
constraints: &RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
) -> RResult<RapidSet<String>> {
for rule in constraints.values() {
for var in rule.defined_vars() {
Expand All @@ -125,9 +125,9 @@ fn check_var_in_constraints<'r>(
Ok(vars)
}

fn check_var_in_transform<'r>(
fn check_var_in_transform(
mut vars: RapidSet<String>,
transform: &'r Option<Transform>,
transform: &Option<Transform>,
) -> RResult<RapidSet<String>> {
let Some(transform) = transform else {
return Ok(vars);
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
Loading