Repository navigation
fix(public): read x-forwarded-for only through a trusted proxy - #627
Open
Evanfeenstra wants to merge 1 commit into
Open
Evanfeenstra wants to merge 1 commit into
Evanfeenstra wants to merge 1 commit into
Conversation
visitorAddress, the key of every per-address budget on the public
surface (requests, launches, tool calls, credit invoices and claims),
took the first x-forwarded-for hop from any client. A client that
reached strut with no proxy overwriting the header could name a new
address each request and get a fresh budget each time.
Now the socket's peer is the visitor unless it is a trusted proxy.
Through a trusted proxy the header is walked from the right, skipping
trusted hops; every hop trusted gives the leftmost. IPv4-mapped peers
are normalized. With no socket the visitor is "unknown" and the header
is not read.
The default trusted set is loopback, link-local and private (Express's
loopback, linklocal, uniquelocal), where a proxy on the same box or
container network connects from. STRUT_TRUSTED_PROXIES (IPs/CIDRs, or
"none") or createStrut({ trustedProxies }) replaces it. A bad entry
fails the boot.
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.
Summary
visitorAddressis the key every per-address budget on the public surface counts under:/publicreads (PUBLIC_REQUESTS_PER_MINUTE), launches (PUBLIC_LAUNCHES_PER_MINUTE), tool calls (PUBLIC_TOOL_CALLS_PER_MINUTE), the credit challenge's invoices and claims,/llms.txt, and the index's listing requests and pings. It used to take the FIRSTx-forwarded-forhop from any client. A client that reached strut with no proxy overwriting that header could name a new address on every request and get a fresh budget each time.It now reads
x-forwarded-foronly when the request came from a trusted proxy:c.env.incoming.socket.remoteAddress) is the visitor, unless it is a trusted proxy. IPv4-mapped IPv6 (::ffff:a.b.c.d) is treated as the IPv4 address.app.fetch), the visitor isunknownand the header is not read.Trusted by default: loopback, link-local and private addresses (RFC 1918, IPv6 unique-local). This is Express's
loopback, linklocal, uniquelocal. A proxy on the same box or container network connects from one of these. Trusting nothing would put every visitor behind such a proxy into one budget.Override:
STRUT_TRUSTED_PROXIES(comma-separated IPs and CIDRs, ornone), read at boot throughbootEnv().createStrut({ trustedProxies })takes precedence over it. Either one replaces the default set rather than adding to it. An entry that is not an IP or a CIDR fails the boot.All of the above lives in the new
src/visitor-address.ts. createStrut builds onevisitorAddressfrom the trusted set and passes it topublicRoutes(new optionalPublicRouteDeps.visitorAddress) and to its own callers (/llms.txt,/index/requests,/index/ping,/public/mcp). The exportedvisitorAddressinpublic-routes.tsis gone. It was not exported from the package.I grepped for other readers of
x-forwarded-for,x-real-ipandforwarded. Every address read already went throughvisitorAddress, including the creditaddress/whoand the public agents'who. The visitor hash inpublic-surface.ts(visitorOfVisit) comes fromx-strut-visit, not from an address.x-forwarded-protoandx-forwarded-host(link origin, theSecurecookie flag) are not the visitor address, so they are unchanged.Behavior changes for operators
STRUT_TRUSTED_PROXIES. Otherwise every visitor behind it shares the proxy's single budget.STRUT_TRUSTED_PROXIESto the proxy's exact address, ornone. The default trusts any private peer.These budgets are budgets, not auth. Priced items still need paid credit, and the in-flight and cost caps are unchanged.
Docs
specs/API.md§17: how the visitor address is chosen.plans/paid-runs.md§3: a "visitor address" paragraph.plans/index-listing.md: points at the new file.AGENTS.md: a file-tree entry and aSTRUT_TRUSTED_PROXIESenv row.Tests
src/visitor-address.test.ts(new):XFF: spoof, realyieldsreal.unknown.none, list) and invalid entries throwing.c.env.incomingthrough Hono.src/public-runs.test.ts, through the real launch route:x-forwarded-for: the 61st answers 429 (too many launches: 60 a minute).::ffff:is still over budget. Another peer has its own budget.STRUT_TRUSTED_PROXIES=nonemakes even loopback's header unread.createStrut({ trustedProxies })takes precedence over the env and replaces the default set.src/public-surface.test.ts: the/publicrequest budget, through a trusted proxy and from a direct client.src/public-tools.test.ts: its helper now sends calls through a loopback proxy, so each call still names its own visitor.With the old
visitorAddressbehavior restored, the four new route-level tests fail.Ran locally: full suite (
npm test): 4351 tests, 4297 pass, 0 fail, 54 skipped.tsc --noEmitis clean.