IP address trusted from a header
CWE-290CWE-348OWASP A01:2021Updated September 4, 20265 min read
Behind a proxy, the visitor's real IP address sits in a header such as X-Forwarded-For. That header is accepted just as readily when a client sends it themselves. If your application trusts it without knowing who set it, the attacker picks their own IP address and bypasses rate limits, blocks and access rules.
Almost every application now runs behind a load balancer, a reverse proxy or a CDN. As a result the application no longer sees the visitor’s address but the proxy’s, and the real address travels in a header. That solution works, until someone fills in that header themselves. The art is telling who set it.
Why can the header not be trusted?
When a proxy forwards a request, it adds the original visitor’s address to a header, usually X-Forwarded-For. The application reads that value and thereby knows who the request came from. That is how it is meant to work.
The point is that a header is part of the request. A client can send it themselves, and a proxy encountering an existing value usually appends its own data rather than replacing it. The header is therefore a list, whose beginning was set by the client and whose end was set by your own infrastructure.
We speak of an IP address trusted from a header when an application takes the first value from that list, or takes the whole value at face value, without knowing which part came from its own proxy. Compare it with a parcel bearing several stamped senders: the one from the distribution centre you can trust, the one the sender wrote on it themselves you cannot. Whoever reads the top one reads what the sender wanted them to read.
How does an attacker abuse X-Forwarded-For?
Vulnerable:
function ipOf(req) {
// The first value: precisely the one the client can determine
const chain = req.headers['x-forwarded-for'];
return chain ? chain.split(',')[0].trim() : req.socket.remoteAddress;
}
app.post('/login', async (req, res) => {
const ip = ipOf(req);
if (await tooManyAttempts(ip)) return res.status(429).send('Too many attempts');
// ...
});
// And an access rule based on the same address
app.use('/admin', (req, res, next) => {
if (ipOf(req).startsWith('10.0.')) return next(); // 'internal'
res.sendStatus(403);
});
Two measures both resting on the same untrustworthy source. The attacker simply sends a header themselves:
POST /login HTTP/1.1
X-Forwarded-For: 203.0.113.7
A different address on every attempt, and the rate limit never counts past one. A password attack that should have stopped after five attempts runs on unhindered. And the access rule on the admin panel is bypassed even more directly:
GET /admin/users HTTP/1.1
X-Forwarded-For: 10.0.4.12
HTTP/1.1 200 OK
The attacker has declared themselves an internal system, and the application believes them.
Safe:
// The number of proxies in front of the application is known and fixed
app.set('trust proxy', 2); // for example: CDN plus own load balancer
app.post('/login', async (req, res) => {
const ip = req.ip; // counts back from the end of the list
if (await tooManyAttempts(ip)) return res.status(429).send('Too many attempts');
// ...
});
# And the proxy overwrites what the client sent
location / {
proxy_set_header X-Forwarded-For $remote_addr; # replace, do not append
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://application;
}
The application now counts back the known number of trusted proxies from the end of the list, taking exactly the value its own infrastructure added. And the proxy replaces the header rather than appending to it, so whatever the client sent disappears entirely.
For the access rule on the admin panel, one further point holds: an IP address is an indication, not proof of identity. Use it at most as an additional condition on top of real authentication and authorisation.
What is the impact of a trusted IP header?
The severity runs from medium to high, depending on what the address is used for. If it only feeds statistics, the damage stays at polluted figures. As soon as security decisions rest on it, it becomes serious.
The most common consequences are bypassing rate limits and blocks. An attacker changing their address per request can try passwords endlessly, guess codes or retrieve data without ever hitting a limit. The measure exists but never counts through.
More serious is bypassing access rules. Systems shielding an admin interface, a monitoring page or an internal API on the basis of an IP range hand that access to anyone sending the right header. That is a direct route to functionality regarded as shielded.
Finally there is the logging side. If the address is recorded from an untrustworthy header, an attacker can make their traces point at any other address. In an incident investigation that leads to the wrong conclusion, and sometimes to an innocent third party.
How do you detect a trusted IP header?
A tester sends requests with a self-chosen X-Forwarded-For and sees whether the application behaves differently: is the rate limit counted afresh, does the address change in a response or a log line, or does a shielded route suddenly become accessible?
They also try the variants. Besides X-Forwarded-For there are X-Real-IP, X-Client-IP, X-Originating-IP, Forwarded and True-Client-IP; applications often look at several and sometimes trust one the proxy does not set at all. What happens with multiple values in the list, and with an invented value at the front, is tested too. Beyond that they check whether the proxy replaces or appends the header, and whether the application uses the right number of proxies. AssistSec pays particular attention to access rules resting on an IP range, because that is where the difference lies between a bypassed limit and bypassed authorisation.
How do you prevent a trusted IP header?
- Configure your application with the exact number of trusted proxies in front of it.
- Have your own proxy replace the forwarding header rather than append to it.
- Never take the first value from the list; count back from the end.
- Ignore the header entirely when your application sits directly on the internet.
- Never use an IP address as the sole basis for access; it is an indication, not an identity.
- Restrict access to internal functionality with network segmentation, a VPN or mutual authentication.
- Watch for the alternative headers your framework may take into account.
- Base rate limits on the account where possible, and on the IP address only for anonymous requests.
- Record in your log files which value was used and where it came from.
Sources
Frequently asked questions
How do I determine the real IP address then?
By knowing how many trusted proxies sit in front of your application and counting back that number from the end of the list. The values your own proxy added are reliable; anything the client sent themselves sits at the front and is not.
Why is this worse than just a bypassed limit?
Because the IP address is used in many systems for access rules, for recognising trusted locations and in log files. An attacker who picks their address can pose as an internal system and make their traces point at another address.
May I ignore the header entirely?
If your application sits directly on the internet, that is the right choice: use the connection's address. If a proxy sits in front, you need the header, but only the part your own proxy added.
What if there are several proxies?
Then the count is what matters. Configure your framework with the exact number of trusted proxies, so it counts back precisely that many positions. A setting that trusts all proxies amounts to trusting the client.
Related articles
- VulnerabilitiesCWE-284A01:2021Broken access controlBroken access control explained: horizontal and vertical privilege escalation, forced browsing, and how deny by default fixes it server-side.
- VulnerabilitiesCWE-307A07:2021Brute force and credential stuffingBrute force and credential stuffing explained: how attackers guess passwords or replay leaked logins, and how throttling and MFA shut both attacks down.
- VulnerabilitiesCWE-644A03:2021Host header injectionIf your application builds links from the Host header, the attacker decides where they point. Learn how that hijacks password resets.
- VulnerabilitiesCWE-770A04:2021No rate limiting on the APIWithout rate limiting an API can be queried without end. Learn how that leads to data theft, cost abuse and outages.
- VulnerabilitiesCWE-16A05:2021Security misconfigurationSecurity misconfiguration explained: how default passwords, debug modes and open cloud buckets let attackers in, and how to harden your systems.