ASVS 5.0 V4.1.3 (level 2) requires that any HTTP header your application relies on, and that an intermediary such as a load balancer, reverse proxy or backend for frontend sets, cannot be overridden by the end user. Think of X-Real-IP, X-Forwarded-For, X-Forwarded-Proto and X-User-ID.
In practice this means the edge must delete or overwrite those headers on every inbound request, and your app must only read them when the request really came from that edge.
Why it matters
Applications read these headers to decide who the caller is and where they come from. If a client can send X-Forwarded-For: 127.0.0.1, your IP allowlist, rate limiter and audit log all believe it. If a client can send X-User-ID: 1, and a downstream service trusts it, that is full account takeover with one curl flag.
The mistake is usually invisible in testing, because the proxy adds its header and everything works. The bug only shows when someone adds their own copy before the proxy does.
How to implement it
Do three things.
First, make the edge overwrite rather than append for identity headers. In nginx:
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-User-ID "";
proxy_pass http://app;
}
If another trusted proxy sits in front of this one, use the real IP module with an explicit list of trusted addresses instead of appending blindly.
Second, make the app trust only known proxies. In Express:
app.set('trust proxy', '10.0.0.0/8'); // your load balancer range only
Third, for internal identity headers such as X-User-ID, do not use plain headers across a trust boundary. Use a signed token that the backend verifies, or network rules so only the gateway can reach the service.
How to test it
Send forged headers directly at the public hostname and watch what the app records.
curl -s https://app.example.com/whoami \
-H "X-Forwarded-For: 203.0.113.9" \
-H "X-Real-IP: 203.0.113.9" \
-H "X-User-ID: 1"
The response and logs must show your real client IP and your real identity, not the forged values. Then try to reach the backend service directly, bypassing the proxy, with the same headers. That direct path should be blocked by the network or reject the request.
Common mistakes
- Using
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_forat the first hop, which keeps whatever the client sent. - Setting
trust proxytotruein Express so every hop is trusted. - Reading the left most value of a forwarded list, which the client controls, instead of the right most trusted entry.
- Leaving internal services reachable from the internet so the proxy can be skipped.
- Trusting
X-Forwarded-Protofor security decisions without checking where it came from.
Quick check
A client sends X-User-ID: 1 and the app believes it. What is the fix?
Strip or overwrite the header at the edge, and make the backend accept identity only from a signed token or from the gateway over a private network.
Sources
- OWASP Application Security Verification Standard, requirement V4.1.3 (level 2)
- RFC 7239, Forwarded HTTP Extension