# CodeWhale: image_analyze follows workspace symlinks, leaking external file bytes

CodeWhale's image_analyze joined image_path lexically instead of canonicalizing, so a workspace symlink named *.png leaked outside-workspace file bytes to the vision endpoint.

- Payout: Research
- Severity: high
- Program: CodeWhale
- Found by: fg0x0
- Disclosed: 2026-09-04
- CVE: CVE-2026-75914
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-w7wx-5q49-r59w
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-w7wx-5q49-r59w
- Tags: area:ai domain:ai domain:desktop impact:data-leak kind:research platform:ghsa sev:high tech:rust technique:incomplete-fix technique:source-review vuln:info-disclosure vuln:path-traversal

**Target:** image_analyze tool in CodeWhale TUI (crates/tui/src/vision/tools.rs)
**Root cause:** image_analyze resolves image_path with a bare context.workspace.join after a lexical component check that rejects Prefix/RootDir/ParentDir but never canonicalizes, so symlinks inside the workspace are followed at read time. Every peer file tool routes through ToolContext::resolve_path, which canonicalizes and enforces workspace containment.
**Steps:**
1. Create workspace and an outside file: `outside/secret.txt` containing a marker.
2. Create a symlink inside the workspace with an image extension: `ln -s ../outside/secret.txt workspace/screenshot.png`.
3. Invoke the tool with `{"image_path": "screenshot.png"}`; components are `[Normal("screenshot.png")]` so the lexical guard passes.
4. `read_image_file` calls `tokio::fs::read` on the joined path, following the symlink and reading the outside file.
5. Bytes are base64-encoded into `data:<mime>;base64,<bytes>` in the `image_url.url` field and POSTed to `${base_url}/chat/completions` with the user's Authorization header.
6. Compare: the same symlink via `read_file` returns `ToolError::PathEscape`.
**How found:** Noticed image_analyze used `context.workspace.join` directly while peer tools like image_ocr used `context.resolve_path`; the earlier Windows fix (commit 2326220) tightened the lexical guard but missed that the site never canonicalized at all.
**Exploitation:** Precondition is a workspace containing a symlink with an image extension pointing outside the workspace (cloned repo shipping symlinks, `ln -s` staging, git clone with core.symlinks=true). Because the tool is ReadOnly and the trait default maps that to ApprovalRequirement::Auto, the model can invoke it with no user prompt; a prompt injection via poisoned README, fetched page, or MCP output issues `{"image_path": "screenshot.png"}` and the target file's bytes egress to the configured vision provider, any TLS-terminating proxy, or a MITM middlebox in the same turn.
**Escalation:** Prompt injection (README, web page, MCP server output) turns the auto-approved read into unattended exfiltration of arbitrary outside-workspace files to a third-party endpoint.
**Insight:** When a codebase has a central path resolver, grep for every file-read site that bypasses it — a lexical component check (Prefix/RootDir/ParentDir) is not containment because it never resolves symlinks; diff sibling tools' resolution calls to spot the odd one out.

