{"record":{"id":"86c8309949f06cb0","repo":"zeroclaw-labs/zeroclaw","slug":"attachment-path-escapes-workspace","errorCode":null,"errorMessage":"attachment path {} escapes workspace {}","messagePattern":"attachment path (.+?) escapes workspace (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/wechat.rs","lineNumber":1113,"sourceCode":"            return Self::canonicalize_within_workspace(&resolved, workspace_dir, target);\n        }\n\n        // Absolute paths are allowed only if they are already inside the workspace.\n        let candidate = Path::new(target);\n        if candidate.is_absolute() {\n            let normalized = normalize_lexical(candidate);\n            if !normalized.starts_with(&workspace_normalized) {\n                ::zeroclaw_log::record!(\n                    WARN,\n                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)\n                        .with_outcome(::zeroclaw_log::EventOutcome::Unknown),\n                    &format!(\n                        \"attachment path {} escapes workspace {}, rejected\",\n                        target,\n                        workspace_dir.display()\n                    )\n                );\n                anyhow::bail!(\n                    \"attachment path {} escapes workspace {}\",\n                    target,\n                    workspace_dir.display()\n                );\n            }\n            return Self::canonicalize_within_workspace(&normalized, workspace_dir, target);\n        }\n\n        // Relative paths are resolved under the workspace root.\n        let resolved = resolve_under(workspace_dir, target).with_context(|| {\n            format!(\n                \"attachment path {} escapes workspace {}\",\n                target,\n                workspace_dir.display()\n            )\n        })?;\n        Self::canonicalize_within_workspace(&resolved, workspace_dir, target)\n    }","sourceCodeStart":1095,"sourceCodeEnd":1131,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/wechat.rs#L1095-L1131","documentation":"`resolve_local_attachment_path` confines WeChat attachment targets to the channel's `workspace_dir`. This bail fires when an absolute target path, after lexical normalization (`normalize_lexical`), does not start with the normalized workspace root — the same message is also attached (via `with_context`) when `resolve_under` rejects `..` traversal in `/workspace/...` or relative targets. It is the first, lexical stage of the attachment sandbox; the symlink stage is `canonicalize_within_workspace`.","triggerScenarios":"Sending a WeChat attachment with an absolute target like `/etc/passwd` or `/home/user/secret.txt` when `workspace_dir` is `/workspace`; a relative target such as `../../etc/passwd` or a `/workspace/../../etc/passwd` form whose `..` segments resolve outside the root via `resolve_under`; a `file://` absolute path outside the workspace after the `file://` prefix is stripped. Any local (non-URL) target passed to `load_attachment_payload` that is not lexically containable in `workspace_dir`.","commonSituations":"Agent tooling generates absolute file paths (temp files in `/tmp`, model outputs in `$HOME`) and passes them as WeChat attachment targets; prompt-injection content tries to attach `/etc/shadow` or `~/.ssh/id_rsa`; misconfigured `workspace_dir` (defaulting somewhere unexpected) so previously-valid absolute paths are now outside it; migrating configs between machines with different home directories.","solutions":["Express the target relative to the workspace root (e.g. `reports/2026-08.png`) or with the `/workspace/` prefix form (`/workspace/reports/2026-08.png`).","Copy or move the file into `workspace_dir` first, then send the in-workspace path.","Set/verify the WeChat channel's `workspace_dir` configuration so it covers the directory holding your files.","If the goal is to share an externally hosted file, pass an `https://` URL as the attachment target instead of a local path — remote targets skip this check."],"exampleFix":"// before: absolute path outside the workspace\nlet attachment = WeChatAttachment { target: \"/tmp/render.png\".into(), kind: WeChatAttachmentKind::Image };\nsend(&channel, attachment).await; // -> attachment path /tmp/render.png escapes workspace /workspace\n\n// after: workspace-relative target\nlet attachment = WeChatAttachment { target: \"render.png\".into(), kind: WeChatAttachmentKind::Image }; // file at /workspace/render.png\nsend(&channel, attachment).await; // ok","handlingStrategy":"validation","validationCode":"// reject/normalize targets before they reach the channel\nfn workspace_relative(target: &str, workspace: &std::path::Path) -> Option<String> {\n    let t = target.trim().strip_prefix(\"file://\").unwrap_or(target.trim());\n    if t.starts_with(\"https://\") || t.starts_with(\"http://\") {\n        return Some(t.to_string()); // remote targets bypass the sandbox\n    }\n    let candidate = std::path::Path::new(t);\n    if candidate.is_absolute() {\n        return candidate.strip_prefix(workspace).ok().map(|p| p.to_string_lossy().into_owned());\n    }\n    if std::path::Path::new(t).components().any(|c| matches!(c, std::path::Component::ParentDir)) {\n        return None; // reject `..` traversal outright\n    }\n    Some(t.to_string())\n}","typeGuard":"fn is_contained_target(target: &str, workspace: &std::path::Path) -> bool {\n    let t = target.trim().strip_prefix(\"file://\").unwrap_or(target.trim());\n    let p = std::path::Path::new(t);\n    if p.is_absolute() {\n        let norm = normalize(p); // resolve `.`/`..` lexically\n        norm.starts_with(workspace)\n    } else {\n        !p.components().any(|c| matches!(c, std::path::Component::ParentDir))\n    }\n}","tryCatchPattern":"match channel.send(msg_with_attachment(target)).await {\n    Err(err) if err.to_string().contains(\"escapes workspace\") => {\n        // log the rejected target as a policy violation; never retry it as-is\n        tracing::warn!(target, \"attachment rejected by workspace sandbox\");\n        return Ok(());\n    }\n    other => other?,\n}","preventionTips":["Always emit workspace-relative attachment targets from your tooling; never absolute paths from temp dirs.","Strip or reject `..` components at the point where targets are produced (agent tool output, message templates).","Treat occurrences of this error as a security signal (possible prompt injection attempting file exfiltration), not as a transient failure.","Keep workspace_dir pinned in config so absolute-path producers can validate against the same root."],"tags":["security","path-traversal","filesystem","wechat","attachment","sandbox"],"backgroundTag":"path-traversal","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}