Skip to content

Close scope-gate bypass: curl/wget were invisible to the PreToolUse hook - #48

Merged
ankitsingh015 merged 1 commit into
mainfrom
claude/scope-gate-curl-wget
Aug 26, 2026
Merged

Close scope-gate bypass: curl/wget were invisible to the PreToolUse hook#48
ankitsingh015 merged 1 commit into
mainfrom
claude/scope-gate-curl-wget

Conversation

@ankitsingh015

Copy link
Copy Markdown
Owner

Summary

User pointed out that my earlier "raw bash bypasses scope/budget/audit enforcement" claim needed verification, not just assertion — specifically asked to make sure bypass isn't possible where it actually matters (scope). Checked the real mechanism (scripts/hooks/scope_gate_hook.py, wired via .claude/settings.json's PreToolUse hook with matcher "*" — it does intercept every Bash call, not just MCP tool calls). Found the actual gap: its TIER2_BASH_TOOLS set only lists the 8 binaries HuntMCP's own MCP servers shell out to. curl was never in it — exactly the tool the recon-skills library (reviewed earlier this session) uses throughout. A raw curl https://out-of-scope-target.com/... was completely invisible to the scope gate.

Fixed by adding curl/wget to the checked set. That surfaced two more real false-positive risks along the way (both verified with actual reproductions before fixing):

  1. Ordinary dev curls would now false-block (curl https://raw.githubusercontent.com/...) — added a DEV_INFRA_HOSTS allowlist (GitHub/PyPI/npm/Go-proxy/Debian-Ubuntu mirrors), same pattern as the existing SAFE_TEST_HOSTS exemption.
  2. The most common curl shapes would false-block on their own argumentscurl -o results.json https://target.com/... was extracting results.json as if it were a second hostname (the existing hostname regex can't distinguish a domain from a filename by pattern alone). Fixed with real URL parsing (urlsplit) instead of blanket regex, plus a small file-extension denylist for the bare-hostname fallback path. Also added evil.com/attacker.com/malicious.com to the safe-host list, since a CORS PoC's -H "Origin: https://evil.com" names the attacker's own probe origin, not a live target.

One deliberate remaining tradeoff, documented in code comments: the fallback bare-hostname scan (for non-URL args like subfinder -d target.com) is still a blanket regex over the whole command line, not argument-aware. Distinguishing "the real target" from "a quoted header value" by stripping quoted substrings was considered and rejected — it risks also stripping a legitimately-quoted target URL, which fails OPEN instead of just over-blocking. A false-positive block is the safe failure mode here; a false-negative allow is not.

Test plan

  • 8 new regression tests: curl/wget now caught, dev-infra exemption, URL-path-not-a-second-host, output-filename-not-a-host (both -o file and -d @file shapes), attacker-origin-placeholder exemption, plus end-to-end main()-level block/allow for a raw curl
  • All 19 pre-existing scope_gate_hook tests still pass unchanged
  • Full 213-test suite passes
  • Manually reproduced every case (bypass, false-positives, fixes) with actual function calls before and after, not just reasoning about it
  • ARCHITECTURE.md's scope-enforcement row updated to describe the current binary list and the correction, matching this repo's existing "Correction" documentation convention

scope_gate_hook.py's TIER2_BASH_TOOLS only listed the 8 binaries HuntMCP's
own MCP servers shell out to (subfinder/httpx/katana/nmap/nuclei/sqlmap/
dalfox/ffuf). curl/wget were left out on the reasoning that no dedicated
MCP server wraps them -- but that meant a raw `curl https://target.com/...`
bypassed scope enforcement entirely, invisible to the hook. Surfaced while
reviewing an external curl-heavy skill library earlier this session.

Added curl/wget to the checked set, plus three things needed to make that
safe rather than just noisy:
- DEV_INFRA_HOSTS allowlist (GitHub/PyPI/npm/Go-proxy/Debian-Ubuntu
  mirrors) so ordinary package-fetching curls stay exempt, same pattern as
  the existing SAFE_TEST_HOSTS exemption for example.com/localhost.
- Real URL parsing (urlsplit) instead of a blanket regex over the whole
  command line, so a URL's own path segment (.../file.txt) is never
  mistaken for a second hostname requiring scope.
- A file-extension denylist for the bare-hostname fallback scan, so
  `curl -o results.json` / `-d @payload.json` -- the two most common curl
  invocation shapes -- don't false-positive on the output/input filename.

evil.com/attacker.com/malicious.com were added to the existing safe-host
list too: a CORS/CSRF PoC's `-H "Origin: https://evil.com"` names the
attacker's own probe origin in a header value, not a live target.

Extraction is still a blanket regex for non-URL bare-domain args
(subfinder -d target.com style) -- deliberately not trying to distinguish
"target" from "quoted header value" there by stripping quotes, since that
risks also stripping a legitimately-quoted target URL and failing OPEN
instead of just over-blocking. A false-positive block is the safe failure
mode; a false-negative allow is not.
@ankitsingh015
ankitsingh015 merged commit 3aca0b2 into main Aug 26, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant