{"record":{"id":"b9800da36869fbdd","repo":"zeroclaw-labs/zeroclaw","slug":"unsafe-sop-path-component-b9800d","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":447,"sourceCode":"                    .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(())\n}\n\nfn hash_sop_dir(dir: &Path) -> Result<String> {\n    let mut hasher = Sha256::new();\n    for name in [\"SOP.toml\", \"SOP.md\"] {\n        let path = dir.join(name);\n        hasher.update(name.as_bytes());\n        hasher.update([0]);\n        if path.exists() {\n            hasher.update(fs::read(path)?);\n        }\n        hasher.update([0]);\n    }\n    Ok(format!(\"sha256:{}\", hex::encode(hasher.finalize())))\n}\n","sourceCodeStart":429,"sourceCodeEnd":465,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/procedural_memory.rs#L429-L465","documentation":"ensure_relative_component checks that a single path component (the slug produced by slugify(sop_name)) consists only of Normal components. It runs after the empty-slug check in contained_sop_dir, so it fires when the slug is non-empty but is itself a special component - '.' or '..' (CurDir/ParentDir). It is the last guard before sops_root.join(slug).","triggerScenarios":"create_proposal/apply_proposal with a sop_name that slugifies to '.' or '..' - names consisting of dots such as '.', '..', or names whose only surviving characters after slugification are dots (e.g. '.-.' if hyphens are stripped).","commonSituations":"Placeholder or joke SOP names made of dots, model-generated drafts with '.' names, or names relying on characters the slugifier removes, leaving only dots behind.","solutions":["Use a SOP name with letters/digits so the slug is a normal component, e.g. 'deploy-check'","Pre-validate in your draft-creation code: reject names where Path::new(&slugify(name)).components() is not a single Normal component","If it fires during apply_proposal, fix the stored proposal's sop_name and re-propose"],"exampleFix":"// before\ncreate_proposal(&engine, ProposalDraft { sop_name: \"..\".into(), .. })?;\n\n// after\ncreate_proposal(&engine, ProposalDraft { sop_name: \"rollback drill\".into(), .. })?;","handlingStrategy":"validation","validationCode":"use std::path::{Path, Component};\nfn is_normal_component(slug: &str) -> bool {\n    Path::new(slug).components().all(|c| matches!(c, Component::Normal(_)))\n}\n// gate: valid_sop_name(name) = is_normal_component(&slugify(name)) && !slug.is_empty()","typeGuard":"fn slug_is_normal(slug: &str) -> bool {\n    std::path::Path::new(slug)\n        .components()\n        .all(|c| matches!(c, std::path::Component::Normal(_)))\n}","tryCatchPattern":"Err(e) if e.to_string().contains(\"unsafe SOP path component\") => {\n    // reject dot-only names upstream; do not sanitize by appending characters blindly\n}","preventionTips":["Include dot-only names in your SOP-name validation test matrix","Treat slugify output as untrusted until checked with the same Normal-component rule","Prefer strict slug regexes for generated names"],"tags":["sop","slug","path-traversal","security","validation"],"backgroundTag":"unsafe-path-component","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}