{"record":{"id":"1d3cb44a3147b706","repo":"Hmbown/CodeWhale","slug":"external-credential-path-escapes-its-absolute-root","errorCode":null,"errorMessage":"external credential path escapes its absolute root: {}","messagePattern":"external credential path escapes its absolute root: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/config/src/external_credentials.rs","lineNumber":129,"sourceCode":"    } else {\n        std::env::current_dir()\n            .map_err(|err| anyhow::anyhow!(\"resolving external credential path: {err}\"))?\n            .join(path)\n    };\n\n    // Normalize only lexical `.` / `..` components. Canonicalization would\n    // inspect a credential path before consent exists and would also silently\n    // bless a symlink target. The secure reader rejects symlink/reparse-point\n    // components when the granted capability is actually consumed.\n    let mut normalized = PathBuf::new();\n    for component in absolute.components() {\n        match component {\n            Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),\n            Component::RootDir => normalized.push(component.as_os_str()),\n            Component::CurDir => {}\n            Component::ParentDir => {\n                if !normalized.pop() {\n                    bail!(\n                        \"external credential path escapes its absolute root: {}\",\n                        quote_os_path(&absolute)\n                    );\n                }\n            }\n            Component::Normal(part) => normalized.push(part),\n        }\n    }\n    if !normalized.is_absolute() {\n        bail!(\n            \"external credential path must resolve to an absolute path: {}\",\n            quote_os_path(&normalized)\n        );\n    }\n    Ok(normalized)\n}\n\n/// The side-effect envelope Codewhale may use for an external credential.","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/config/src/external_credentials.rs#L111-L147","documentation":"Thrown by resolve_external_credential_path while lexically normalizing an absolute credential path: a `..` component would climb above the root, meaning the path escapes its declared absolute root. The library refuses such paths without any filesystem I/O, closing a classic path-traversal hole in external credential lookup.","triggerScenarios":"An ExternalCredentialSource path like /safe/root/../../etc/passwd, or a symlink-free path string containing enough `..` segments to underflow the root (e.g. /../secret) passed to resolve_external_credential_path via validate_read_scope/read_grant.","commonSituations":"Config entries or tool-supplied paths that embed relative escapes; attackers or buggy scripts composing credential paths from untrusted fragments.","solutions":["Rewrite the path so it stays under its absolute root, removing excess `..` segments","Point the external credential source at the actual absolute file path","If indirection is needed, move the root up so the relative portion stays inside it","Reject or sanitize the path at configuration time before it reaches read_grant"],"exampleFix":"// before\nlet path = \"/secure/creds/../../etc/passwd\";\nresolve_external_credential_path(path)?;\n// after\nlet path = \"/secure/creds/token.txt\";\nresolve_external_credential_path(path)?;","handlingStrategy":"validation","validationCode":"fn stays_under_root(path: &str, root: &str) -> bool {\n    std::path::Path::new(root).join(path)\n        .components()\n        .try_fold(0usize, |depth, c| match c {\n            std::path::Component::ParentDir => if depth == 0 { None } else { Some(depth - 1) },\n            std::path::Component::Normal(_) => Some(depth + 1),\n            _ => Some(depth),\n        }).is_some()\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never compose credential paths from untrusted input","Sanitize or reject `..` segments in user-supplied paths","Pin external credential sources to explicit absolute files"],"tags":["security","path-traversal","filesystem"],"backgroundTag":"path-traversal-blocked","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}