50

Alright, I have an small authentication issue. My web service allows to connect to my API over HTTP with a username and password, but this connection can also be restricted to a specific IP address.

This means that the $_SERVER['REMOTE_ADDR'] can be incorrect. I already know that any IP information can never truly be relied upon - I have the restriction only in an attempt to add another layer of security.

If this is the general overview of a request to my web server:

clientSERVER => clientPROXY => myPROXY => mySERVER

Then this means that mySERVER shows REMOTE_ADDR of myPROXY instead of that of the client and sends the actual IP of the client as HTTP_X_FORWARDED_FOR.

To overcome this, my web service has a list of 'trusted proxy' IP addresses and if REMOTE_ADDR is from one of those trusted IP addresses, then it tells my web service that the actual IP address is the value of HTTP_X_FORWARDED_FOR.

Now the problem is with clientPROXY. This means that (quite often) mySERVER gets HTTP_X_FORWARDED_FOR value that has multiple IP addresses. According to HTTP_X_FORWARDED_FOR documentation, the value is a comma-separated list of IP addresses where the first IP is that of the actual true client and every other IP address is that of a proxy.

So, if HTTP_X_FORWARDED_FOR has multiple values and my service is IP restricted, do I have to check the 'last' value of HTTP_X_FORWARDED_FOR against my allowed IP list and just ignore the actual client IP?

I assume that in a system, where I have to set the list of allowed IP addresses, the whitelisted IP address should be that of a proxy and not an IP that is behind the proxy (since that could be some localhost IP and change frequently).

And what of HTTP_CLIENT_IP?

1

7 Answers 7

44

You can use this function to get the proper client IP.

function get_client_ip() { if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) { return $_SERVER['HTTP_X_FORWARDED_FOR']; } else if (array_key_exists('REMOTE_ADDR', $_SERVER)) { return $_SERVER['REMOTE_ADDR']; } else if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) { return $_SERVER['HTTP_CLIENT_IP']; } return ''; } 

Alternatively, using PHP 7+ you can write the function using null coalescing operators.

function get_client_ip() { return $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? $_SERVER['HTTP_CLIENT_IP'] ?? ''; } 
Sign up to request clarification or add additional context in comments.

4 Comments

This answer doesn't address the specific situation described in the question, where not all requests are passed through a proxy. As a result, requests received directly from clients may contain inaccurate IP addresses in headers.
Nice answer, you can find even accurate from Prestashop's Tools class :) . Find getRemoteAddr() function
This is a possible security hole. Anybody can add the X-Forwarded-For header to their request.
@frodeborli : You're right ; that's why one should use the reqidel+reqadd directives in haproxy.
26

In the light of the latest httpoxy vulnerabilities, there is really a need for a full example, how to use HTTP_X_FORWARDED_FOR properly.

So here is an example written in PHP, how to detect a client IP address, if you know that client may be behind a proxy and you know this proxy can be trusted. If you don't known any trusted proxies, just use REMOTE_ADDR

<?php function get_client_ip () { // Nothing to do without any reliable information if (!isset ($_SERVER['REMOTE_ADDR'])) { return NULL; } // Header that is used by the trusted proxy to refer to // the original IP $proxy_header = "HTTP_X_FORWARDED_FOR"; // List of all the proxies that are known to handle 'proxy_header' // in known, safe manner $trusted_proxies = array ("2001:db8::1", "192.168.50.1"); if (in_array ($_SERVER['REMOTE_ADDR'], $trusted_proxies)) { // Get the IP address of the client behind trusted proxy if (array_key_exists ($proxy_header, $_SERVER)) { // Header can contain multiple IP-s of proxies that are passed through. // Only the IP added by the last proxy (last IP in the list) can be trusted. $proxy_list = explode (",", $_SERVER[$proxy_header]); $client_ip = trim (end ($proxy_list)); // Validate just in case if (filter_var ($client_ip, FILTER_VALIDATE_IP)) { return $client_ip; } else { // Validation failed - beat the guy who configured the proxy or // the guy who created the trusted proxy list? // TODO: some error handling to notify about the need of punishment } } } // In all other cases, REMOTE_ADDR is the ONLY IP we can trust. return $_SERVER['REMOTE_ADDR']; } print get_client_ip (); ?> 

5 Comments

This is an excellent answer, but there is one minor problem. If strict error reporting is on, trying to trim(end(explode())) on one line will return "Only variables should be passed by reference." To get around that, set the exploded proxy header to a variable first, then trim(end()) that instead.
@BenDyer Thanks, fixed it.
I believe this is incorrect. According to Mozilla, the original IP is the first, not the last, so it should be trim(array_shift()) developer.mozilla.org/en-US/docs/Web/HTTP/Headers/…
@LucasBustamante While it's correct that in normal circumstances, the "original IP" is the leftmost, that IP can't be trusted because malicious actors can send a falsified X-Forwarded_For header. Any fake addresses in this header will be the leftmost addresses. If it is important to avoid such potential forgeries, you need the rightmost address (besides any known & trusted proxy addresses). See the "Trusted proxy count/Trusted proxy list" sections of that MDN document you mentioned.
@thelr this is why a "trusted proxy" is one that you have set up to that it strips any X-Forwarded_For it receives from the clients, thus resetting the chain, and starts it anew by inserting the current client IP (as given by the socket). Then there can be other proxies in the chain, which have no such config, but are set up to only accept connections from the trusted proxy. Then the correct IP address will be the first one. The lst IP is never to be used, as it will be the one of the Proxy itself.
26

I like Hrishikesh's answer, to which I only have this to add...because we saw a comma-delimited string coming across when multiple proxies along the way were used, we found it necessary to add an explode and grab the final value, like this:

$IParray=array_values(array_filter(explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']))); return reset($IParray); 

the array_filter is in there to remove empty entries.

2 Comments

Note that it appears that using the last value in the list is still probably using a proxy's IP. According to the link below, the originating client is the FIRST IP. en.wikipedia.org/wiki/X-Forwarded-For
Also note that the same source says that this is easy to forge, so the last one is more reliable. So each use case may make different choices. If the use case for getting the IP is combating fraud or spam, the first IP may be meaningless and the most reliable address - the last one - is most useful. If the use case for getting the IP is less nefarious activities, the first one would be most useful.
3

You can also solve this problem via Apache configuration using mod_remoteip, by adding the following to a conf.d file:

RemoteIPHeader X-Forwarded-For RemoteIPInternalProxy 172.16.0.0/12 LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined 

Comments

1

Please note that the "HTTP_X_FORWARDED_FOR" can be spoofed very easy to change the entry for example with Browser Plugins ... so please be aware!

Try this modified Code from Hrishikesh Mishra (not tested):

 function getClientIP(){ if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER) && preg_match('/\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}\b/', $_SERVER["HTTP_X_FORWARDED_FOR"])){ return $_SERVER["HTTP_X_FORWARDED_FOR"]; }else if (array_key_exists('REMOTE_ADDR', $_SERVER)) { return $_SERVER["REMOTE_ADDR"]; }else if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) { return $_SERVER["HTTP_CLIENT_IP"]; } return ''; } 

1 Comment

should it edited so the preg match supports ipv6 too?
0

If you use it in a database, this is a good way:

Set the ip field in database to varchar(250), and then use this:

$theip = $_SERVER["REMOTE_ADDR"]; if (!empty($_SERVER["HTTP_X_FORWARDED_FOR"])) { $theip .= '('.$_SERVER["HTTP_X_FORWARDED_FOR"].')'; } if (!empty($_SERVER["HTTP_CLIENT_IP"])) { $theip .= '('.$_SERVER["HTTP_CLIENT_IP"].')'; } $realip = substr($theip, 0, 250); 

Then you just check $realip against the database ip field

Comments

-4

HTTP_CLIENT_IP is the most reliable way of getting the user's IP address. Next is HTTP_X_FORWARDED_FOR, followed by REMOTE_ADDR. Check all three, in that order, assuming that the first one that is set (isset($_SERVER['HTTP_CLIENT_IP']) returns true if that variable is set) is correct. You can independently check if the user is using a proxy using various methods. Check this out.

5 Comments

None of these headers is "more reliable" than the others. They can all be forged by the client, and your applications needs to know what's trusted and what's not: you normally know your infrastructure and the IP address of any proxy / load balancer in front of your HTTP server(s).
True, but generally, if an order of preference is to be established based on assumed reliability, this is it. I realize that they can all be manipulated, but if these must be used, this is how to do it.
Why is there so much disinformation on this topic? $_SERVER['REMOTE_ADDR'] is the only reliable field that's not influenced by the remote user, all others are parsed from headers.
@transistor09 Care to provide your own answer then?
On my site, $_SERVER['HTTP_CLIENT_IP'] is not set and $_SERVER['REMOTE_ADDR'] changes on every page load, while $_SERVER['HTTP_X_FORWARDED_FOR'] is constant and set to the expected value.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.