{"record":{"id":"c32fff06126769e3","repo":"zeroclaw-labs/zeroclaw","slug":"unsafe-sop-path-component","errorCode":null,"errorMessage":"unsafe SOP path component '{}'","messagePattern":"unsafe SOP path component '(.+?)'","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/sop/procedural_memory.rs","lineNumber":431,"sourceCode":"    let mut remainder: Vec<&std::ffi::OsStr> = Vec::new();\n    let mut current = path;\n    loop {\n        if current.exists() {\n            let mut resolved = fs::canonicalize(current)\n                .with_context(|| format!(\"canonicalize '{}'\", current.display()))?;\n            for name in remainder.iter().rev() {\n                resolved.push(name);\n            }\n            return Ok(resolved);\n        }\n        match (current.file_name(), current.parent()) {\n            (Some(name), Some(parent)) => {\n                let component = Path::new(name);\n                if component\n                    .components()\n                    .any(|c| !matches!(c, Component::Normal(_)))\n                {\n                    bail!(\"unsafe SOP path component '{}'\", name.to_string_lossy());\n                }\n                remainder.push(name);\n                current = parent;\n            }\n            _ => bail!(\"cannot resolve SOP path '{}'\", path.display()),\n        }\n    }\n}\n\nfn ensure_relative_component(component: &str) -> Result<()> {\n    let path = Path::new(component);\n    if path\n        .components()\n        .any(|c| !matches!(c, Component::Normal(_)))\n    {\n        bail!(\"unsafe SOP path component\");\n    }\n    Ok(())","sourceCodeStart":413,"sourceCodeEnd":449,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/procedural_memory.rs#L413-L449","documentation":"resolve_existing_ancestor walks a path upward via file_name()/parent(), collecting the existing part of the path, and requires every collected final component to be a Normal component (a plain name). This error means a component being resolved is not a normal file name - it is '..', '.', a root separator, or a Windows prefix. It is a low-level guard inside ensure_within_root that rejects structurally unsafe paths before any symlink resolution.","triggerScenarios":"contained_sop_dir/ensure_within_root receiving a sops_root or target whose final component is '..' or '.' (path literally ending in '../..', '/.', etc.) or carries a prefix like 'C:\\' - typically from a sops_dir config value like '/', '..', 'C:\\sops\\', or from a slug/path that survived earlier checks but ends in a dot component.","commonSituations":"Misconfigured sops_dir in the ZeroClaw config (relative '..' entries, bare drive prefix on Windows), or hand-constructed paths passed through install_root that end in a dot or dot-dot segment.","solutions":["Fix the sops_dir (and install_root) configuration to be a normal absolute path with no '.', '..', or trailing-separator-only components","Normalize candidate paths with std::path::PathBuf components and reject non-Normal components before calling apply_proposal","On Windows, drop the drive-prefix form and pass a plain rooted path"],"exampleFix":"# before (config)\nsops_dir = \"../sops/..\"\n\n# after\nsops_dir = \"/home/user/.zeroclaw/sops\"","handlingStrategy":"validation","validationCode":"fn all_normal_components(p: &Path) -> bool {\n    p.components().all(|c| matches!(c, std::path::Component::Normal(_)))\n}","typeGuard":null,"tryCatchPattern":"Err(e) if e.to_string().contains(\"unsafe SOP path component\") => {\n    // normalize the configured path (remove '..', '.', trailing separators) and retry once\n}","preventionTips":["Normalize config paths with components().collect() and reject non-Normal parts at load time","Never build SOP paths via string concatenation of user input","Validate sops_dir/install_root in config loading, not at write time"],"tags":["sop","path-safety","symlink","security","config"],"backgroundTag":"unsafe-path-component","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}