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

Lark/Feishu marker target resolves outside workspace_dir

Error message

Lark/Feishu marker target resolves outside workspace_dir

What it means

After canonicalizing both workspace_dir and the candidate path, validate_lark_marker_target requires candidate.starts_with(&workspace) — the file must physically live inside the workspace. Any absolute path outside the workspace, ../ traversal, or a symlink inside the workspace that points outside canonicalizes to an escaping path and bails (WARN reason=outside_workspace). This is a path-traversal containment check protecting the upload surface.

Source

Thrown at crates/zeroclaw-channels/src/lark.rs:639

                    .with_attrs(::serde_json::json!({"reason": "not_found"})),
                "lark: marker target not found on disk"
            );
            anyhow::Error::msg("Lark/Feishu marker target not found on disk")
        } else {
            anyhow::Error::msg(format!(
                "canonicalize Lark/Feishu marker target failed: {err}"
            ))
        }
    })?;

    if !candidate.starts_with(&workspace) {
        ::zeroclaw_log::record!(
            WARN,
            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
                .with_attrs(::serde_json::json!({"reason": "outside_workspace"})),
            "lark: marker target escapes workspace_dir"
        );
        anyhow::bail!("Lark/Feishu marker target resolves outside workspace_dir");
    }

    Ok(candidate)
}

fn resolve_lark_media_marker(
    marker: &LarkOutgoingMediaMarker,
    workspace_dir: Option<&Path>,
) -> anyhow::Result<LarkResolvedMediaMarker> {
    let path = validate_lark_marker_target(&marker.target, workspace_dir)?;
    let metadata = std::fs::metadata(&path).map_err(|err| {
        anyhow::Error::msg(format!(
            "read Lark/Feishu marker target metadata failed: {err}"
        ))
    })?;
    if !metadata.is_file() {
        anyhow::bail!("Lark/Feishu marker target is not a file");
    }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Make every file-producing tool write inside workspace_dir and emit workspace-relative paths in markers
  2. Replace symlinks inside the workspace with real files (or copies) — canonicalize follows links and the resolved target must remain inside
  3. If a legitimate shared asset lives outside, copy it into the workspace before attaching
Defensive patterns

Strategy: validation

Validate before calling

// Containment pre-check mirroring the channel's rule:
let ws = std::fs::canonicalize(workspace_dir)?;
let cand = std::fs::canonicalize(ws.join(target.trim()))?;
anyhow::ensure!(cand.starts_with(&ws), "marker escapes workspace");

Try / catch

if let Err(e) = channel.send(&msg).await {
    if e.to_string().contains("resolves outside workspace_dir") {
        // copy the asset into the workspace, rewrite the marker target, retry
    }
}

Prevention

When it happens

Trigger: Marker target like "../../etc/passwd", "/tmp/secret.png" (absolute, outside workspace), or "assets/link.png" where link.png is a symlink to ~/private/key.png — canonicalize resolves the real location, it fails the starts_with check, and send/finalize_draft abort the upload. Pinned by lark_marker_target_rejects_workspace_escape.

Common situations: Tools that write artifacts to /tmp and reference them absolutely; macOS symlinked folders inside the workspace; agent attempting to attach dotfiles via relative traversal.

Related errors


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