{"record":{"id":"281dba92b8325af9","repo":"Hmbown/CodeWhale","slug":"bundle-path-candidate-is-absolute-only-paths-inside-the","errorCode":null,"errorMessage":"bundle path {candidate:?} is absolute; only paths inside the config directory are accepted","messagePattern":"bundle path (.+?) is absolute; only paths inside the config directory are accepted","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/cli/src/config_bundles.rs","lineNumber":718,"sourceCode":"\n/// Resolve `candidate` inside `base_dir`, refusing traversal and symlink\n/// escapes. Returns the resolved path or an error naming the refusal — the\n/// candidate string itself is safe to echo (it is config data, not a secret).\n/// Resolve `candidate` inside `base_dir`, refusing traversal and symlink\n/// escapes. Returns the joined path or an error naming the refusal.\n/// Reserved for path-carrying bundle sections (none shipped yet); exercised\n/// by the traversal tests so the contract cannot silently rot.\n#[cfg_attr(\n    not(test),\n    expect(dead_code, reason = \"path-carrying sections land with the next schema\")\n)]\npub fn resolve_bounded_path(base_dir: &Path, candidate: &str) -> Result<PathBuf> {\n    if candidate.contains('\\0') {\n        bail!(\"bundle path contains a NUL byte; refused\");\n    }\n    let candidate_path = Path::new(candidate);\n    if candidate_path.is_absolute() {\n        bail!(\n            \"bundle path {candidate:?} is absolute; only paths inside the config directory are accepted\"\n        );\n    }\n    let canonical_base = base_dir\n        .canonicalize()\n        .with_context(|| format!(\"config directory {} is unavailable\", base_dir.display()))?;\n    let joined = base_dir.join(candidate_path);\n    // Walk the joined path's ancestors from the deepest existing component up:\n    // every existing component must canonicalize inside the base, so a symlink\n    // pointing outside the config directory is refused even when the final\n    // target does not exist yet.\n    let deepest_existing = joined\n        .ancestors()\n        .find(|ancestor| ancestor.symlink_metadata().is_ok())\n        .context(\"bundle path has no existing ancestor inside the config directory\")?;\n    let resolved = deepest_existing.canonicalize().with_context(|| {\n        format!(\n            \"could not resolve bundle path component {}\",","sourceCodeStart":700,"sourceCodeEnd":736,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/cli/src/config_bundles.rs#L700-L736","documentation":"A path carried inside a config bundle is absolute. `resolve_bounded_path` only accepts relative paths that stay inside the config directory, so any absolute path (e.g. `/etc/passwd`, `C:\\...`) is refused before resolution. This prevents a malicious bundle from writing outside the config directory.","triggerScenarios":"Importing a bundle whose path section contains an absolute path; passing an absolute `candidate` string to `resolve_bounded_path(base_dir, candidate)`.","commonSituations":"A bundle exported on another machine that embedded absolute paths; a hand-edited bundle entry; a third-party bundle attempting to escape the config directory.","solutions":["Edit the bundle so the path is relative to the config directory (e.g. `settings/providers.json`).","Re-export the bundle with the current CLI, which writes only relative bounded paths.","If generating bundles yourself, strip or relativize any absolute path before embedding it."],"exampleFix":"// before\n\"path\": \"/home/alice/.codewhale/settings.json\"\n// after\n\"path\": \"settings.json\"","handlingStrategy":"validation","validationCode":"fn is_relative(candidate: &str) -> bool {\n    !std::path::Path::new(candidate).is_absolute()\n}","typeGuard":"fn is_bounded_candidate(candidate: &str) -> bool {\n    let p = std::path::Path::new(candidate);\n    !p.is_absolute() && !candidate.contains('\\0')\n}","tryCatchPattern":"match resolve_bounded_path(&base_dir, candidate) {\n    Ok(path) => apply(path),\n    Err(e) if e.to_string().contains(\"is absolute\") => log::warn!(\"skipped absolute bundle path: {candidate:?}\"),\n    Err(e) => return Err(e),\n}","preventionTips":["Store only paths relative to the config directory inside bundles.","When exporting from scripts, strip the config-directory prefix from absolute paths.","Reject absolute paths in any bundle you accept from third parties."],"tags":["path-validation","security","config-bundle"],"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-22T06:17:15.046Z"}