Network identity
Published
Almost everything your application does per-client depends on one question that has no reliable answer: who is the client. Rate limits, IP allowlists, geo rules, abuse scoring and every audit row are keyed on it. The settings that answer that question are two or three small options, and they fail in opposite directions - which is what made this the sharpest area we measured this pass.
What the sample actually does
The file configures a client-address resolver with four options: trust two proxy hops, only honour the forwarded header when the socket peer is inside the internal range, read the standard forwarded-for header, and trust the forwarded protocol header. It then resolves four requests - two genuine customers arriving through the load balancer, an attacker also arriving through the load balancer with a forged entry prepended to the header, and an attacker reaching the origin directly with a forged header.
There is an administrative route in front of an allowlist for the internal jump range. The load balancers live in that same range. That is not a contrived detail; it is the ordinary shape of a small deployment, and it is what turns one of the arms below from an inconvenience into an incident.
The resolver is copied unprotected and its defaults mirror the framework's: trusting proxies is off, so the client is the socket peer; there is no restriction on which peers may set the forwarded header once you do trust hops; and the forwarded protocol header is not trusted, so a request through a TLS-terminating balancer reads as plain HTTP.
In the unprotected run every decision is correct. Both customers resolve to their real addresses, both attackers resolve to the attacker's real address, the forged header is ignored, and the admin allowlist admits nobody.
One rename, and the admin allowlist admits the internet
Renaming trustProxy turned trusting off. Every request then resolved to the socket peer, which behind a load balancer is the load balancer: all three requests arriving through it became 10.0.0.2.
That address is inside the internal range the admin allowlist permits. So admin-allows-customer went from false to true, and so did admin-allows-attacker-via-lb. One renamed option name, and a route protected by an IP allowlist became reachable by anybody who can reach the load balancer - which is everybody.
The collateral damage is almost as bad and much easier to miss. The rate-limit bucket key for both a customer and an attacker became the same string. Distinct buckets across our four requests dropped from three to two, so a per-client limit had become a per-datacentre limit shared by every caller on the internet. Every audit row recorded 10.0.0.2 as the client. Nothing threw, nothing was logged as an error, and every request still succeeded.
Renaming trustProxy together with the CIDR restriction produced exactly the same result, because once nothing is trusted the restriction is never consulted. That is a dead-code interaction worth knowing about: an option can be inert simply because a different option is set, and a pattern that takes both looks identical to a pattern that takes one.
The opposite failure, from the neighbouring option
Renaming trustedProxyCidrs did the reverse. The restriction on which peers may set the forwarded header stopped existing, so the header was honoured from anybody - including the attacker who reached the origin directly, bypassing the balancer entirely.
That request had a single forged entry claiming an internal address. It went from resolving to the attacker's real address to resolving to 10.0.0.5, which is inside the allowlisted range. admin-allows-direct-spoof went from false to true and spoofed-header-honoured went from false to true.
So the same admin route can be opened from either direction, by two different option names, with two different mechanisms. One makes everyone look internal by accident; the other lets anyone claim to be internal on purpose. An audit that checks only one of them proves nothing about the other.
It is also the arm that argues hardest for defence in depth on this particular control. An IP allowlist in front of an administrative route depends on a chain of assumptions - the origin is not directly reachable, the proxy strips inbound forwarded headers, the framework is configured to trust exactly the right number of hops - and any one of those failing is sufficient. Authentication in front of that route does not depend on any of them.
The loud one
Renaming trustForwardedProto is the arm you would find in staging. The resolved protocol dropped from https to http for every request, because the origin only ever speaks plain HTTP to the balancer and the header saying otherwise was no longer trusted.
Our model reports what happens next: https-redirect-loop went from false to true. An application that redirects insecure requests to their secure equivalent, behind a terminator that already terminated, redirects to the URL it is already on, forever. Secure-flagged cookies would fail to be set on the same reasoning.
It is the only arm in this area that produces an immediate, obvious, total outage - and it is by far the least dangerous of the three, which is a useful thing to sit with. The arms that break the site get fixed on the day. The arms that quietly widen an allowlist do not get fixed until somebody looks.
The remaining option, the name of the forwarded header itself, was pinned to the same value as the default and produced no change, as expected.
What the result object does
Renaming the fields on what the resolver hands back - the resolved address, the protocol and the trusted flag - made every one of them read as undefined. The addresses printed as undefined, the trusted flag printed as undefined, and the secure check evaluated false.
This is loud in the logs and silent in the decisions, which is the worst combination of the two. An allowlist comparison against an undefined address will not match, so the admin route fails closed; but a bucket key of ip:undefined puts every caller into one shared bucket, and every audit row records nothing.
Renaming the fields on the request objects being passed in threw a TypeError immediately, consistent with everything else we have measured in this direction: names going in degrade, names coming out crash, and names read off a structure you built yourself do neither until somebody serialises them.
What to do about it
Exclude the option names your address resolver reads, and the fields on what it returns. It is a small, stable, easily identified list.
Then test the identity rather than the response. Send a request with a forged forwarded header from an address that is not a proxy, and assert that the resolved client is the socket peer rather than the forged value. Send a request through your real proxy chain and assert that the resolved client is the original caller rather than the balancer. Both assertions are two lines and both fail on the arms above.
Most importantly, do not let an IP allowlist be the only thing in front of an administrative route. Every arm in this article moved that allowlist's answer, in one direction or the other, without producing an error - and the control that would have held through all of them is authentication.
Frequently asked questions
Does obfuscation change how my server identifies clients?
Not by itself. Protection alone, on all five presets we tested including both output targets and the compressed profile, produced behaviour identical to the unprotected file. Every result here required member renaming pointed at the option names the resolver reads.
What was the worst result in this area?
Renaming the option that enables proxy trust. Every caller then resolved to the load balancer's address, which sat inside the internal range an administrative allowlist permits, so a route protected by that allowlist became reachable by anyone who can reach the balancer.
Does that also affect rate limiting?
Yes, and in the same direction. Both a customer and an attacker produced the same bucket key, so a per-client limit became a limit shared by every caller arriving through that balancer. Distinct buckets across our sample dropped from three to two.
How can the same route be opened from the other direction?
By renaming the option that restricts which peers may set the forwarded header. It then gets honoured from anybody, including a caller reaching the origin directly, so a single forged entry claiming an internal address passed the allowlist.
Which arm would we notice first?
The forwarded-protocol one, because it produces an immediate redirect loop behind a TLS-terminating balancer. It is also the least dangerous of the three, which is the uncomfortable part - the arms that break the site get fixed the same day, and the arms that widen an allowlist do not.
What should I exclude from member renaming?
The option names your resolver reads - the trust setting, the trusted-peer list, the header name and the protocol switch - plus the fields on the resolved object your logging and access checks consume.
What is the durable fix?
Do not let an IP allowlist be the only control in front of an administrative route. Every arm we measured moved that allowlist's answer in one direction or the other without raising an error, and authentication holds through all of them.
Related reading