Repository navigation
Conversation
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (2)
🛟 Help
|
Tonours
force-pushed
the
fix/prd-1760-xff-whitelist
branch
from
October 28, 2026 10:57
b4275d2 to
4ec70ca
Compare
This branch has not been deployed
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.

What changed
The IP whitelist now resolves the client address through a new
resolveClientIp(socketIp, forwardedFor)helper (packages/agent/src/routes/security/client-ip.ts), aligned with how Rails computesremote_ip(agent-ruby parity).X-Forwarded-Forwhile each hop is a trusted proxy (loopback, RFC1918 private, link-local, ULA). The first untrusted hop is the address the whitelist checks. A forged header sent from a public socket is therefore ignored: the caller is judged on its socket address.::ffff:10.0.0.3,::ffff:a00:3, uppercase included): Node'sBlockListmatches mapped forms against IPv4 subnets natively (verified on the Node version CI pins), so the helper delegates to it instead of slicing the prefix textually — the hex notation used to stop the walk.req.socket.remoteAddressand the raw header, so it is the same for every mount (standalone, Express, Fastify, Koa, NestJS) whatever the host'strust proxy/app.proxysettings.X-Forwarded-For), so in-process calls stay exempt.Side effect: a legitimate client that sends its own
X-Forwarded-Forbehind an appending proxy used to be refused (the comma list failed theBlockListcheck); it is now judged on the address the proxy appended.Behaviour change to note: when the agent sits directly behind a proxy with a public address (or a CGNAT peer), the whitelist now checks that proxy's address. The 403 message names the address it checked.
Security notes
context.request.ip) was rejected: the agent serves its routes through its ownnew Koa()withproxyoff (framework-mounter.ts), socontext.request.ipis always the socket address — every deployment behind a load balancer (Heroku router, ALB, nginx, Cloud Run) would have been locked out.trustedProxiesoption is the natural follow-up if this shows up in the wild (not added now: new public API, no reported need).remote_ip/ agent-ruby; it needs network position plus a stolen token.X-Forwarded-Forwithout appending still passes a forged value — outside the agent's control.Sibling agents (cross-agent audit, 2026-10-28)
X-Forwarded-Forfirst, e.g.django_agent/utils/converter.py,flask_agent/utils/requests.py) — tracked as PRD-1839.remote_ip(same walk-from-the-right + skip-trusted algorithm as this fix).Tests
client-ip.test.ts: trusted-proxy walk (single hop, appending proxy with a forged left entry, multi-hop chain, all-trusted chain, mapped forms in dotted/hex/uppercase notation, mapped public socket), invalid hops (malformed, port-bearing, empty slot, unreadable socket), spacing, repeated header.ip-whitelist.test.ts: every context carries an explicit socket peer; the 403 assertions check the exact address; the anti-spoof exemption test asserts the socket address in the message.203.0.113.77+ forgedX-Forwarded-For: 9.9.9.9against a9.9.9.9rule) was confirmed red against the pre-fix code before the fix landed.yarn workspace @forestadmin/agent test→ green;lint→ 0 errors;build(tsc strict) → ok.Council review (2026-10-28)
Round 1 (head
4ec70cada): three independent phase-A passes (code review, maintainability, cross-family) plus the validator arbiter. 13 distinct candidates, 1 confirmed: closing PRD-1760 left the agent-python follow-up tracked by nothing — fixed by opening PRD-1839 and linking it above. The other 12 were rejected as pre-existing behaviour (403 message echoing the hop, port-bearing hops, loopback exemption predating this PR), explicit residual risks documented above, or speculative. Two pragmatic adoptions from rejected-but-convergent candidates: dropping the textual::ffff:unmapping for nativeBlockListmatching (simpler, and fixes the hex-notation walk edge; verified against the CI-pinned Node) and sharing oneipVersionhelper. Findings deliberately left: unifying the loopback exemption with the proxy-trust model and port-stripping hops (both pre-PR behaviour, fail-closed, pinned by the plan), test-structure restructuring around the mocked context.fixes PRD-1760
Definition of Done
General
Security