{"record":{"id":"daff6579cc98b66f","repo":"zeroclaw-labs/zeroclaw","slug":"invalid-sop-name-name-must-be-a-single-path-c","errorCode":null,"errorMessage":"invalid SOP name '{name}': must be a single path component (no separators, '.', '..', or absolute paths)","messagePattern":"invalid SOP name '(.+?)': must be a single path component \\(no separators, '\\.', '\\.\\.', or absolute paths\\)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/sop/mod.rs","lineNumber":233,"sourceCode":"            let expanded = shellexpand::tilde(dir);\n            install_root.join(expanded.as_ref())\n        }\n        _ => default_sops_dir(install_root),\n    }\n}\n\n/// Resolve `<sops_dir>/<name>`, accepting only a single normal path\n/// component so caller-controlled names cannot escape the SOP root.\nfn resolve_sop_dir(sops_dir: &Path, name: &str) -> Result<PathBuf> {\n    let mut components = Path::new(name).components();\n    let single_normal = matches!(\n        (components.next(), components.next()),\n        (Some(std::path::Component::Normal(_)), None)\n    );\n    if single_normal && !name.contains(['/', '\\\\', '\\0']) {\n        Ok(sops_dir.join(name))\n    } else {\n        anyhow::bail!(\n            \"invalid SOP name '{name}': must be a single path component (no separators, '.', '..', or absolute paths)\"\n        )\n    }\n}\n\n// ── SOP loading ─────────────────────────────────────────────────\n\n/// Load all SOPs from the configured directory, resolved against `install_root`.\npub fn load_sops(\n    install_root: &Path,\n    config_dir: Option<&str>,\n    default_execution_mode: SopExecutionMode,\n) -> Vec<Sop> {\n    let dir = resolve_sops_dir(install_root, config_dir);\n    load_sops_from_directory(&dir, default_execution_mode)\n}\n\n/// Load a single SOP by directory name from the SOPs root. Errors if the","sourceCodeStart":215,"sourceCodeEnd":251,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/mod.rs#L215-L251","documentation":"resolve_sop_dir validates that an SOP name is exactly one normal path component: it must not contain '/', '\\\\', or NUL, and Path::components() must yield a single Normal component. This rejects '.', '..', absolute paths, Windows prefixes, and any nested path before the name is joined onto sops_dir, preventing path traversal outside the SOP store.","triggerScenarios":"Calling load/save/create/delete SOP APIs with a name like \"team/backup\", \"..\", \".\", \"/etc/passwd\", \"C:\\\\sop\", a name containing '\\0', or any name built by joining user input with path separators.","commonSituations":"Names derived from file paths or URLs; user-supplied names passed unchecked from a CLI or web form; tools that assume Windows separators are safe because the server runs on Linux; empty or dot-only names.","solutions":["Pass a single-component slug: sanitize user input by replacing separators with '-' (or reject it) before calling any SOP API.","Validate early at the trust boundary with your own single-component check so the bad name never reaches the store layer.","Never construct SOP names by joining directory paths; map hierarchical names to flat slugs."],"exampleFix":"// before: name built from a user path segment\nlet sop_name = format!(\"{team}/{name}\"); // 'platform/backup' -> rejected\ncreate_sop(&sops_dir, &sop)?;\n\n// after: flatten to one component\nlet sop_name = format!(\"{}-{}\", team, name); // 'platform-backup'\ncreate_sop(&sops_dir, &sop)?;","handlingStrategy":"validation","validationCode":"use std::path::Path;\n\nfn is_valid_sop_name(name: &str) -> bool {\n    !name.is_empty()\n        && name == name.trim()\n        && !name.contains('/')\n        && !name.contains('\\\\')\n        && !name.contains('\\0')\n        && name != \".\"\n        && name != \"..\"\n        && Path::new(name).components().count() == 1\n}\n\nassert!(is_valid_sop_name(&sop_name), \"SOP name must be one path component\");","typeGuard":"fn is_valid_sop_name(name: &str) -> bool {\n    !name.is_empty()\n        && name == name.trim()\n        && !name.contains('/')\n        && !name.contains('\\\\')\n        && !name.contains('\\0')\n        && name != \".\"\n        && name != \"..\"\n        && std::path::Path::new(name).components().count() == 1\n}","tryCatchPattern":"match save_sop(&sops_dir, &sop) {\n    Err(e) if e.to_string().contains(\"must be a single path component\") => {\n        // sanitize: replace separators with '-' and retry with the flattened slug\n    }\n    other => other?,\n}","preventionTips":["Reject multi-component names at the API/CLI boundary, before they reach the store.","Flatten hierarchical names into slugs ('team-x' not 'team/x').","Never build SOP names from filesystem paths or URLs."],"tags":["sop","name","path-traversal","validation","security"],"backgroundTag":"path-traversal-rejected","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}