{"record":{"id":"2c09ec007feb71ac","repo":"Hmbown/CodeWhale","slug":"state-subdir-must-not-contain-parent-dir-components-subdir","errorCode":null,"errorMessage":"state subdir must not contain parent-dir (..) components: {subdir}","messagePattern":"state subdir must not contain parent-dir \\(\\.\\.\\) components: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/config/src/lib.rs","lineNumber":5741,"sourceCode":"        bail!(\"state subdir must not be empty\");\n    }\n    let path = std::path::Path::new(subdir);\n    if path.is_absolute() {\n        bail!(\"state subdir must not be an absolute path: {subdir}\");\n    }\n    if path.components().any(|c| {\n        matches!(\n            c,\n            std::path::Component::RootDir | std::path::Component::Prefix(_)\n        )\n    }) {\n        bail!(\"state subdir must not contain a root or prefix: {subdir}\");\n    }\n    if path\n        .components()\n        .any(|c| matches!(c, std::path::Component::ParentDir))\n    {\n        bail!(\"state subdir must not contain parent-dir (..) components: {subdir}\");\n    }\n    Ok(())\n}\n\n/// Resolve a state subdirectory, preferring the CodeWhale root if\n/// it already exists, otherwise falling back to the legacy root.\n///\n/// This is the read-path resolver: it returns the primary path when\n/// migration has occurred or on a fresh install, but keeps reading\n/// from the legacy path for users who haven't migrated yet.\npub fn resolve_state_dir(subdir: &str) -> Result<PathBuf> {\n    ensure_safe_state_subdir(subdir)?;\n    let explicit_codewhale_home = codewhale_home_is_explicit();\n    let primary = codewhale_home()?.join(subdir);\n    if explicit_codewhale_home || primary.exists() {\n        return Ok(primary);\n    }\n    let legacy = legacy_deepseek_home()?.join(subdir);","sourceCodeStart":5723,"sourceCodeEnd":5759,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/config/src/lib.rs#L5723-L5759","documentation":"Parent-directory (`..`) components in a state subdir would let callers traverse out of the state root. `ensure_safe_state_subdir` rejects any subdir containing `Component::ParentDir`, even nested relative paths like `\"a/b\"` remain allowed.","triggerScenarios":"Calling a state-path helper with a subdir containing `..` segments, e.g. `\"../shared\"` or `\"sessions/../../etc\"`.","commonSituations":"Constructing paths with `..` to reach a sibling directory of the state root; user-supplied relative paths passed through unvalidated; template strings with `..` placeholders left unsubstituted.","solutions":["Use only `Normal` path components relative to the state root (nested `\"a/b\"` is fine)","Canonicalize the target and recompute it relative to the state root before calling","Use the explicit state-root override API if a different root location is genuinely needed"],"exampleFix":"// before\nlet dir = state_dir_in(\"../other-tool-state\")?;\n// after\nlet dir = state_dir_in(\"sessions\")?;","handlingStrategy":"validation","validationCode":"use std::path::{Component, Path};\nfn no_parent_components(subdir: &str) -> bool {\n    !Path::new(subdir).components()\n        .any(|c| matches!(c, Component::ParentDir))\n}","typeGuard":"fn contained_subdir(subdir: &str) -> Option<&str> {\n    let p = Path::new(subdir);\n    if subdir.is_empty()\n        || p.components().any(|c| !matches!(c, Component::Normal(_)))\n    { None } else { Some(subdir) }\n}","tryCatchPattern":"match state_dir_in(subdir) {\n    Err(e) if e.to_string().contains(\"parent-dir\") => {\n        // reject or recompute the path within the state root\n    }\n    result => result,\n}","preventionTips":["Validate that every component is `Component::Normal` before calling","Canonicalize user paths and verify containment in the state root","Never interpolate user input into subdir strings"],"tags":["path","validation","state"],"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"}