diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..7c721f6 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2024-05-18 - Prevent Root Escalation in Path Normalization +**Vulnerability:** A logic flaw in `crates/flow/src/incremental/extractors/typescript.rs` allowed path components like `..` to unconditionally pop prior components from a manually resolved path. This incorrectly treated absolute roots or relative leading parent directories as normal items, converting safe paths into unintended locations. +**Learning:** Manual path normalization relying simply on `.pop()` for `Component::ParentDir` is flawed because it fails to distinguish between normal path segments and foundational components like `RootDir` (`/`) or `Prefix` (`C:\`). +**Prevention:** To prevent path traversal vulnerabilities when manually normalizing paths with `std::path::Component` (e.g., during module path resolution in `crates/flow`), explicitly block `Component::ParentDir` from popping `Component::RootDir` or `Component::Prefix`. If the components list is empty or its last element is `Component::ParentDir`, the new `Component::ParentDir` must be pushed rather than ignored to correctly preserve relative paths like `../../a`. diff --git a/crates/flow/src/incremental/extractors/typescript.rs b/crates/flow/src/incremental/extractors/typescript.rs index 1bdda4e..4690750 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(); + if let Some(last) = components.last() { + match last { + std::path::Component::Normal(_) => { + components.pop(); + } + std::path::Component::ParentDir => { + components.push(component); + } + _ => { + // Don't pop Prefix or RootDir + } + } + } else { + components.push(component); + } } std::path::Component::CurDir => {} _ => components.push(component), diff --git a/crates/rule-engine/src/check_var.rs b/crates/rule-engine/src/check_var.rs index 9e40105..39d77f2 100644 --- a/crates/rule-engine/src/check_var.rs +++ b/crates/rule-engine/src/check_var.rs @@ -27,8 +27,8 @@ pub enum CheckHint<'r> { pub fn check_rule_with_hint<'r>( rule: &'r Rule, utils: &'r RuleRegistration, - constraints: &'r RapidMap, - transform: &'r Option, + constraints: & RapidMap, + transform: & Option, fixer: &Vec, hint: CheckHint<'r>, ) -> RResult<()> { @@ -56,8 +56,8 @@ pub fn check_rule_with_hint<'r>( fn check_vars_in_rewriter<'r>( rule: &'r Rule, utils: &'r RuleRegistration, - constraints: &'r RapidMap, - transform: &'r Option, + constraints: & RapidMap, + transform: & Option, fixer: &Vec, upper_var: &RapidSet, ) -> RResult<()> { @@ -85,8 +85,8 @@ fn check_utils_defined( fn check_vars<'r>( rule: &'r Rule, utils: &'r RuleRegistration, - constraints: &'r RapidMap, - transform: &'r Option, + constraints: & RapidMap, + transform: & Option, fixer: &Vec, ) -> RResult<()> { let vars = get_vars_from_rules(rule, utils); @@ -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, - constraints: &'r RapidMap, + constraints: & RapidMap, ) -> RResult> { for rule in constraints.values() { for var in rule.defined_vars() { @@ -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, - transform: &'r Option, + transform: & Option, ) -> RResult> { let Some(transform) = transform else { return Ok(vars);