feat(security): let operators un-block CIDRs behind a transparent proxy#1465
Open
connermo wants to merge 1 commit into
Open
feat(security): let operators un-block CIDRs behind a transparent proxy#1465connermo wants to merge 1 commit into
connermo wants to merge 1 commit into
Conversation
SSRF protection resolves a hostname and judges the resulting IP. That model assumes DNS resolution describes where the traffic actually goes, which stops being true behind a TUN/fake-IP proxy: every query is answered with a synthetic address out of a reserved range, and the proxy then routes that address to the real public host. The IP is a handle, not a destination. In that environment web_fetch rejects ordinary public sites — observed with news.sina.cn resolving to 198.18.0.236 — and no configuration can fix it, because the block list is compiled in. The agent then burns iterations retrying URLs that can never succeed. GOCLAW_SSRF_ALLOWED_CIDRS lets an operator name the ranges their proxy hands out. Empty by default, so nothing changes for deployments that do not set it, and the accepted and refused entries are both logged at startup — this widens what LLM- and admin-supplied URLs can reach, so it should be visible. Ranges an SSRF actually targets can never be allowlisted: link-local (including cloud metadata at 169.254.169.254), multicast and unspecified are refused at parse time, in either direction, so neither an exact entry nor a wider range that swallows one gets through. Applied inside isBlocked rather than only in validate() because NewSafeClient re-checks the pinned IP at dial time through the same function — relaxing just the pre-flight check would pass validation and then fail to connect. internal/tools carries its own private-range list for web_fetch and web_search, separate from this package and not identical to it. It has to consult the same setting, or relaxing one gate leaves the other rejecting the very traffic the operator permitted. Unifying the two lists is left alone here; it is a wider change than this one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
SSRF protection resolves a hostname and judges the resulting IP. That model assumes DNS resolution describes where the traffic actually goes — which stops being true behind a TUN / fake-IP proxy.
In fake-IP mode the proxy answers every DNS query with a synthetic address out of a reserved range, then routes that address to the real public host. The resolved IP is a handle, not a destination.
Observed on a machine behind such a proxy:
All three were verified reachable from the same container —
HTTP 200— so the network path was fine; only the check rejected them.The block list is compiled in, so no configuration can fix this, and
web_fetchis simply unusable. Worse, the agent keeps burning iterations retrying URLs that can never succeed: one turn took 85s with 15 tool calls, 3 of which were guaranteed failures.Change
GOCLAW_SSRF_ALLOWED_CIDRSlets an operator name the ranges their proxy hands out:Ranges that can never be allowlisted
Link-local (including cloud metadata at
169.254.169.254), multicast, and unspecified are refused at parse time — checked in both directions, so neither an exact entry nor a wider range that swallows one gets through.0.0.0.0/0is refused.This mirrors what
exemptableCIDRsalready declines to cover:Verified against the running binary with a deliberately dangerous value:
Two implementation notes
Applied inside
isBlocked, not justvalidate().NewSafeClientre-checks the pinned IP at dial time through the same function, so relaxing only the pre-flight check would pass validation and then fail to connect.internal/toolshas its own SSRF gate.web_shared.gocarries a separate private-range list used byweb_fetch/web_search, and the two lists are not identical (toolshas100.64.0.0/10andfec0::/10;securityhas224.0.0.0/4). It must consult the same operator setting, or relaxing one gate leaves the other rejecting exactly the traffic the operator permitted — this was caught during testing, when the first version of the change had no effect onweb_fetchat all.Unifying the two lists is deliberately not attempted here; it is a wider and riskier change. Flagging it as pre-existing debt: adding a range to one list today silently misses the other.
Tests
internal/security/ssrf_operator_allowlist_test.go(6 cases) andinternal/tools/web_shared_ssrf_allowlist_test.go(2 cases):0.0.0.0/0, multicast, unspecified, IPv6 link-local are all refusedisBlockedandValidatehonour the allowlist while keeping metadata and RFC 1918 blockedCheckSSRF(thetoolsgate) honours it too, and still blocks metadata / RFC 1918 / loopbackVerification
go build ./...,go build -tags sqliteonly ./...,go vet ./...— cleango test -racegreen forsecurity,tools,mcpnews.sina.cnfetched successfully;169.254.169.254and10.0.0.1still rejected