{"record":{"id":"bd4ce21d994126c5","repo":"ultraworkers/claw-code","slug":"path-escapes-workspace-boundary","errorCode":null,"errorMessage":"path {} escapes workspace boundary {}","messagePattern":"path (.+?) escapes workspace boundary (.+?)","errorType":"validation","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"rust/crates/runtime/src/file_ops.rs","lineNumber":44,"sourceCode":"];\n\n/// Check whether a file appears to contain binary content by examining\n/// the first chunk for NUL bytes.\nfn is_binary_file(path: &Path) -> io::Result<bool> {\n    use std::io::Read;\n    let mut file = fs::File::open(path)?;\n    let mut buffer = [0u8; 8192];\n    let bytes_read = file.read(&mut buffer)?;\n    Ok(buffer[..bytes_read].contains(&0))\n}\n\n/// Validate that a resolved path stays within the given workspace root.\n/// Returns the canonical path on success, or an error if the path escapes\n/// the workspace boundary (e.g. via `../` traversal or symlink).\n#[allow(dead_code)]\nfn validate_workspace_boundary(resolved: &Path, workspace_root: &Path) -> io::Result<()> {\n    if !resolved.starts_with(workspace_root) {\n        return Err(io::Error::new(\n            io::ErrorKind::PermissionDenied,\n            format!(\n                \"path {} escapes workspace boundary {}\",\n                resolved.display(),\n                workspace_root.display()\n            ),\n        ));\n    }\n    Ok(())\n}\n\n/// Text payload returned by file-reading operations.\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct TextFilePayload {\n    #[serde(rename = \"filePath\")]\n    pub file_path: String,\n    pub content: String,\n    #[serde(rename = \"numLines\")]","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/ultraworkers/claw-code/blob/08106b0c3771ef5b4a5aa176acccd460e88b7325/rust/crates/runtime/src/file_ops.rs#L26-L62","documentation":"`validate_workspace_boundary` (runtime/src/file_ops.rs:44) enforces the workspace-containment invariant for the `*_in_workspace` file operations: the resolved path must have the workspace root as a Path-component prefix (`Path::starts_with` is component-based, so `spacex` does NOT sneak past root `space`). Violations return `ErrorKind::PermissionDenied`. It is called from grep/glob/search and the in-workspace write variants (file_ops.rs:325, 348, 361, 407, 436, 686, 699, 714).","triggerScenarios":"A glob or search pattern containing `../` that resolves outside the root; a symlink inside the workspace pointing to a directory outside it; passing an absolute path in `/tmp` or `$HOME` to an in-workspace op; the workspace root itself being non-canonical (given root `~/proj` while resolved paths canonicalize through a symlink to `/home/user/proj`).","commonSituations":"Monorepos where `node_modules` symlinks point to global package stores; `bun`/`pnpm` symlink farms; configs referencing files outside the project; running claw from a symlinked path so the passed root never prefix-matches canonicalized targets.","solutions":["Keep every target path inside the workspace root; drop `../` segments from patterns.","Replace out-of-boundary symlinks with copies, or move the referenced file inside the workspace.","When calling the `_in_workspace` APIs programmatically, canonicalize the root (fs::canonicalize) before passing it so prefix comparison matches canonicalized targets."],"exampleFix":"// before\nlet root = Path::new(\"~/proj\");                       // tilde never expands -> every check fails\nwrite_file_in_workspace(\"/etc/app.conf\", data, root)?; // path ... escapes workspace boundary ...\n\n// after\nlet root = std::fs::canonicalize(shellexpand_home(\"~/proj\"))?;\nlet inside = root.join(\"config/app.conf\");\nwrite_file_in_workspace(&inside.to_string_lossy(), data, &root)?;","handlingStrategy":"validation","validationCode":"fn inside_workspace(target: &Path, root: &Path) -> io::Result<bool> {\n    let root = std::fs::canonicalize(root)?;\n    let target = std::fs::canonicalize(target)?;\n    Ok(target.starts_with(&root))   // component-wise prefix, like the guard\n}","typeGuard":null,"tryCatchPattern":"match write_file_in_workspace(path, data, root) {\n    Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied\n        && e.to_string().contains(\"escapes workspace boundary\") => { /* reject pattern / move file inside root */ }\n    other => other,\n}","preventionTips":["Canonicalize the workspace root before passing it to *_in_workspace APIs","Avoid `../` in glob/search patterns","Audit symlinks inside the workspace (pnpm/bun stores) — they canonicalize outside the root","Component prefix means sibling dirs like proj and proj-2 do NOT match; only real containment passes"],"tags":["filesystem","security","path-traversal","workspace"],"backgroundTag":"path-traversal","analyzedSha":"08106b0c3771ef5b4a5aa176acccd460e88b7325","analyzedAt":"2026-08-18T00:29:38.590Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}