
Reproduction lab for CVE-2026-54316 (Claude Code WebFetch huggingface.co bare-hostname permission bypass / exfiltration)
A self-contained, disposable lab that reproduces
GHSA-fg94-h982-f3mm
/ CVE-2026-54316: Claude Code pre-approved huggingface.co as a bare hostname
for the WebFetch tool, so any path on that domain — including
attacker-controlled model repositories — was fetched without a permission
prompt. Combined with prompt injection, that becomes an out-of-band channel
for exfiltrating data, observed via HuggingFace's server-side download counts.
| Advisory | GHSA-fg94-h982-f3mm |
| CVE | CVE-2026-54316 |
| Package | @anthropic-ai/claude-code (npm) |
| Affected | >= 0.2.54, < 2.1.163 |
| Patched | 2.1.163 |
| Root cause | Bare-hostname allow-listing on a multi-tenant host (CWE-183) |
This reproduces a patched, publicly disclosed vulnerability for educational and defensive purposes. Use it only against infrastructure you own:
fixtures/canary.env) — never use a real one.huggingface.co path with no approval prompt, while every other domain
triggers one — because huggingface.co sits in a hardcoded allow-list.The container is the only place the vulnerable version runs; your host stays clean. Authenticate with a Claude subscription token (no API key needed):
docker build -t cve-2026-54316-lab .
docker run --rm -it cve-2026-54316-lab
Inside the container run claude and pick "Claude account with subscription"
to log in interactively. The vulnerable 2.1.162 predates the
CLAUDE_CODE_OAUTH_TOKEN env var, so a setup-token is not used here — the
interactive browser/paste-code flow avoids minting a long-lived token at all.
The point is the per-domain approval prompt asymmetry, so WebFetch is left
available (it is not denied) — approval is handled by prompting, the default.
Inside the container, in claude:
Use WebFetch to fetch
https://example.comand summarize it.
→ A permission prompt appears asking you to approve example.com. Then:
Use WebFetch to fetch
https://huggingface.co/<your-account>/canary-lab/resolve/main/config.json.
Vulnerable: the huggingface.co fetch proceeds with no prompt, while
example.com required one. That asymmetry is the bug — huggingface.co is in a
hardcoded allow-list. (The HF repo need not exist for this test; a 401/404 still
proves the prompt never fired.) Rebuild with @anthropic-ai/[email protected]
and the huggingface.co fetch now prompts too — that before/after is the headline.
./scripts/make_hf_canary_files.sh ./hf-repo, then push hf-repo/ to a
public HuggingFace repo you own (<your-account>/canary-lab).payloads/untrusted-readme.md, set HF_ACCOUNT, and place it where the
agent will read it as untrusted input.Dockerfile — pinned vulnerable 2.1.162.claude/settings.json — empty allow/deny so WebFetch prompts per-domainfixtures/canary.env — dummy canaryscripts/make_hf_canary_files.sh — HF canary-file layoutpayloads/untrusted-readme.md — prompt-injection payload (sanitized)