zeroclaw-labs/zeroclaw · error · anyhow::Error

Slack outbound attachment path escapes workspace: {}

Error message

Slack outbound attachment path escapes workspace: {}

What it means

Raised by SlackChannel::resolve_outbound_attachment_marker when the canonicalized target path does not start with the canonicalized workspace_dir — a path-traversal containment guard. The target is first canonicalized (resolving .., ., and symlinks, and failing with a 'not found' context if absent), then checked against the canonical workspace prefix; anything escaping the workspace, including via symlink, is refused. This blocks both '../' traversal and symlink escapes.

Source

Thrown at crates/zeroclaw-channels/src/slack.rs:1054

            anyhow::bail!("Slack outbound attachment path must be absolute: {target}");
        }

        let workspace = self
            .workspace_dir
            .as_deref()
            .context("Slack outbound local attachments require workspace_dir")?;
        let canonical_workspace = tokio::fs::canonicalize(workspace).await.with_context(|| {
            format!(
                "failed to canonicalize Slack workspace {}",
                workspace.display()
            )
        })?;
        let canonical_path = tokio::fs::canonicalize(path)
            .await
            .with_context(|| format!("Slack outbound attachment path not found: {target}"))?;

        if !canonical_path.starts_with(&canonical_workspace) {
            anyhow::bail!(
                "Slack outbound attachment path escapes workspace: {}",
                canonical_path.display()
            );
        }

        let metadata = tokio::fs::metadata(&canonical_path)
            .await
            .with_context(|| {
                format!(
                    "failed to stat Slack outbound attachment {}",
                    canonical_path.display()
                )
            })?;
        if !metadata.is_file() {
            anyhow::bail!(
                "Slack outbound attachment target is not a file: {}",
                canonical_path.display()
            );

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Copy or generate the file inside workspace_dir and reference that copy with an absolute path
  2. Sanitize agent-generated markers: reject '..' components and resolve symlinks before composing the message
  3. If shared assets are legitimately needed, place or symlink them into the workspace from the outside — the target path itself must canonicalize inside
Defensive patterns

Strategy: validation

Validate before calling

async fn attachment_stays_in_workspace(target: &str, workspace_dir: &Path) -> anyhow::Result<PathBuf> {
    let path = Path::new(target);
    anyhow::ensure!(path.is_absolute(), "target must be absolute");
    let canon = tokio::fs::canonicalize(path).await?; // resolves .. and symlinks
    let canon_ws = tokio::fs::canonicalize(workspace_dir).await?;
    anyhow::ensure!(canon.starts_with(&canon_ws), "target escapes workspace");
    Ok(canon)
}

Prevention

When it happens

Trigger: A marker target like [file:/workspace/../../etc/passwd], an absolute path that simply lies outside workspace_dir, or a path inside the workspace that is a symlink pointing outside — canonicalize resolves it to the real location, the starts_with check fails, and the bail fires.

Common situations: Untrusted LLM output composing attachment markers that reference system files; templates using ../ to reach shared dirs; symlinks in the workspace created by other tooling pointing to /tmp or home directories.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/d5aef014a3274259. Report an issue: GitHub.