PHP: How do you get the client's IP address?
You need the visitor’s IP address for statistics, for block lists or for rate limiting. The answer looks simple, but there is exactly one reliable source for it — and several that only look like one.
The reliable source
<?php
echo $_SERVER['REMOTE_ADDR'];
?>
REMOTE_ADDR is the address the TCP connection actually comes from. This value
is set by the web server, not by the client, so it cannot be forged.
As long as there is no proxy between you and the visitor, you are done here.
The problem with proxies
If a reverse proxy, a load balancer or a CDN sits in front, the connection comes
from that proxy. REMOTE_ADDR then no longer points at the visitor.
I set this up: curl → nginx as proxy → nginx as web server → PHP-FPM. Queried
once directly and once through the proxy.
Directly, without the proxy:
REMOTE_ADDR 172.21.0.1
HTTP_X_FORWARDED_FOR (not set)
Through the proxy:
REMOTE_ADDR 172.21.0.4 <- the proxy
HTTP_X_FORWARDED_FOR 172.21.0.1 <- the real client
So the proxy has set an additional header containing the original address. And that is exactly why you find snippets like this one all over the web:
<?php
// Careful, this widespread variant is dangerous!
function clientIp(): string {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
}
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
}
return $_SERVER['REMOTE_ADDR'];
}
?>
Why that code is dangerous
HTTP headers come from the client. Anyone can set them. I requested the same page again, this time with invented headers:
user@pc:~$ curl -H 'X-Forwarded-For: 1.2.3.4' -H 'Client-IP: 9.9.9.9' http://localhost:8080/ip.php
The result through the proxy:
REMOTE_ADDR 172.21.0.4
HTTP_X_FORWARDED_FOR 1.2.3.4, 172.21.0.1
HTTP_CLIENT_IP 9.9.9.9
Two things stand out.
First, HTTP_CLIENT_IP simply contains the value I sent. No proxy set that
header. The function above checks it first, though, and would return 9.9.9.9.
An attacker therefore decides entirely what ends up in your logs and block
lists.
Second, and this is the subtler point: X-Forwarded-For now contains two
addresses.
1.2.3.4, 172.21.0.1
^^^^^^^ ^^^^^^^^^^
forged real
The proxy did not replace my invented value, it appended the actual sender
address at the end. That is the intended behaviour, because the header is meant
to describe the whole chain. For you it means the forged value comes first.
And explode(',', $xff)[0] reads exactly that one.
Without a proxy it is even clearer:
REMOTE_ADDR 172.21.0.1 <- still correct
HTTP_X_FORWARDED_FOR 1.2.3.4 <- entirely invented
How to do it properly
The rule is simple: only trust a header if you know who set it.
If there is no proxy in front, use REMOTE_ADDR and nothing else. Full stop.
If your own proxy is in front, first check that the connection really comes from that proxy, and only then evaluate the header:
<?php
function clientIp(): string {
$proxies = ['172.21.0.4']; // the addresses of your own proxies
$remote = $_SERVER['REMOTE_ADDR'];
if (!in_array($remote, $proxies, true)) {
// The connection does not come from our proxy,
// so REMOTE_ADDR is already the client.
return $remote;
}
if (empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return $remote;
}
$chain = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
// The last entry was added by our own proxy and is therefore
// the only one we were not simply handed by the client.
return end($chain);
}
?>
The last point is the important one: you take the last entry of the list, not the first. Everything before it was supplied by the client.
With several chained proxies you have to skip as many entries from the end as you have proxies of your own. And if you use a CDN, check its documentation — most of them set their own header that cannot be appended to.
What about IPv6?
REMOTE_ADDR returns either an IPv4 or an IPv6 address depending on the
connection. So if you store the address in a database, size the field
accordingly: an IPv6 address in text form can be up to 45 characters long.
Summary
- Without a proxy:
REMOTE_ADDRonly, everything else can be forged. - Never use
HTTP_CLIENT_IP; in practice only an attacker sets it. - With your own proxy: first check that
REMOTE_ADDRis that proxy. - Take the last entry from
X-Forwarded-For, not the first.
About Netcup (advertisement)
The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).