Skip to content

fix(runtime-node): relay rate limit keys the real visitor on any host - #33

Merged
sagnik11 merged 2 commits into
mainfrom
fix/relay-trust-proxy-hardening
Oct 25, 2026
Merged

sagnik11 merged 2 commits into
mainfrom
fix/relay-trust-proxy-hardening

Conversation

@sagnik11

@sagnik11 sagnik11 commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

What this fixes for users

The browser relay's rate limit can no longer be dodged, and it works out who each visitor is on any host without setup. Today the relay keys each visitor on the first X-Forwarded-For entry, which any caller sets freely. A script that sends a new value on every request gets a fresh 120-a-minute window each time, so it can push unbounded traffic through the relay under the site's private ingest key and burn its telemetry quota.

How the relay now picks the visitor

New option trustProxy, default "auto". Proxies append the address they saw to the right of X-Forwarded-For, or overwrite the header, so the relay reads it right to left and takes the first public address, skipping hops on the customer's own network (private, loopback, link-local, carrier-grade NAT, unique-local). A caller can only add entries to the left of what its proxy appended, so it cannot pick its own key.

Where the relay runs What "auto" keys
Node, reached directly The connection's own address; the header is ignored
Node behind a load balancer or ingress on a private network (AWS, GCP, Azure, Render, Railway, Fly, Heroku, Kubernetes, nginx) The address that proxy appended
Next.js route handlers, Vercel, Netlify, Deno Deploy and other fetch runtimes The rightmost public entry, which the platform's edge wrote
Cloudflare Workers CF-Connecting-IP, trusted only when the runtime set request.cf

Other settings for setups "auto" cannot see:

  • trustProxy: <count> when exactly that many proxies the customer controls sit in front, with Express trust proxy semantics. 1 covers a Node origin behind Cloudflare with no load balancer.
  • trustProxy: true keeps the old leftmost behaviour, for chains that strip client values.
  • trustProxy: false never reads the header: the socket address in Node, one shared bucket in fetch runtimes.
  • clientIp(request) supplies the address directly, for example Deno's info.remoteAddr or Bun's server.requestIP.
  • Env-var strings are read too ("auto", "false", "0", "2"). Anything unrecognised falls back to "auto" with one warning, so "false" can no longer turn trust on by being truthy.

Known limit. A fetch handler exposed straight to the internet with no proxy in front receives the caller's header untouched, so it can still be fooled. The README says to use trustProxy: false or clientIp there.

Platform facts relied on: Vercel overwrites X-Forwarded-For, Cloudflare appends the connecting address and sets CF-Connecting-IP, AWS load balancers append the last hop by default.

Where this came from

@rajeshaipython-stack wrote the original hardening after #20 merged. The follow-up commits went to their fork branch and never reached main.

Part Status
Stop trusting the first X-Forwarded-For entry Ported here, with an automatic default instead of a shared bucket
Secret redaction in relay context Already on main through #26, with tests
Detached cancel on oversized bodies Already on main through #20, with tests
JWT literal removed from tests Already on main

The first commit ports the contributor's strict version. The second replaces its shared-bucket default with "auto", because the strict default put every visitor behind a load balancer or platform proxy into one 120-a-minute window.

Tests

11 new relay cases cover:

  • the forged left part of the header;
  • separate visitors behind overwriting and appending proxies;
  • Cloudflare Workers with and without request.cf;
  • false and its env strings;
  • hop counts;
  • true;
  • the clientIp resolver;
  • Node direct, behind a private proxy including IPv4-mapped peers, with false, and behind Cloudflare with 1.
Suite Result
npm run test -w @autter/runtime-next 14 passed
npm run test -w @autter/runtime-node, locally 174 passed, 2 failed

Both local failures are process-exit timing tests: autoflush "second signal exits immediately" and compat "a hanging compat request never delays process exit". Each spawns a child Node process with a 2.5 s limit, and both missed it while the machine's load average was 43. They do not touch the relay, and both passed in this PR's CI on the first commit. CI on this commit is the check.

No version bump or release is included.

🤖 Generated with Claude Code

sagnik11 and others added 2 commits October 25, 2026 16:22
…by default

The browser relay keyed its per-IP rate limit on the first X-Forwarded-For
entry, which any caller can set. Rotating it gave a fresh window on every
request, so the limit did not bound parsing and forwarding under the
server's ingest key.

New option `trustProxy` (only the boolean `true` enables it). Without it
the Node handler keys the real socket peer and the fetch handler keys one
shared bucket for the route. Behind a proxy that overwrites the header,
set `trustProxy: true` to key each client.

Ported from the relay hardening by rajeshaipython-stack that followed
#20 on the fork and never reached main. Its context redaction and
detached oversize cancel already landed through #26 and #20.

Co-authored-by: rajeshaipython-stack <rajesh.ai.python@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n any host

The strict default from the previous commit kept the limit safe but put
every visitor behind a proxy into one window. `trustProxy` now defaults
to "auto": read X-Forwarded-For right to left and take the first public
address, which the nearest load balancer, CDN or platform appended or
wrote. In Node, a connection straight from a public address is the
visitor and its header is ignored. On Cloudflare Workers, use
CF-Connecting-IP when the runtime set request.cf.

`trustProxy` also takes a hop count, `true` (leftmost entry) and
`false`, including the same values as strings from env vars, and a new
`clientIp(request)` option supplies the address directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sagnik11 sagnik11 changed the title fix(runtime-node): relay rate limit no longer trusts X-Forwarded-For by default fix(runtime-node): relay rate limit keys the real visitor on any host Oct 25, 2026
@sagnik11
sagnik11 merged commit 754e08d into main Oct 25, 2026
6 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