46

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?

Thibault
  • 1,566
  • 15
  • 22
kingmaple
  • 4,200
  • 5
  • 32
  • 44

7 Answers7

39

You can use this function to get proper client IP:

public function getClientIP(){       
     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 '';
}
Andrew Bucklin
  • 699
  • 8
  • 19
Hrishikesh Mishra
  • 3,295
  • 3
  • 27
  • 33
  • 9
    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. –  Oct 26 '14 at 00:43
  • Nice answer, you can find even accurate from Prestashop's Tools class :) . Find getRemoteAddr() function – Vipul Hadiya Apr 22 '15 at 06:58
  • 8
    This is a possible security hole. Anybody can add the X-Forwarded-For header to their request. – frodeborli Jul 04 '18 at 09:29
  • @frodeborli : You're right ; that's why one should use the reqidel+reqadd directives in haproxy. – tisc0 Sep 21 '18 at 07:26
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.

Philip
  • 6,827
  • 13
  • 75
  • 104
user336828
  • 494
  • 5
  • 4
  • 8
    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. http://en.wikipedia.org/wiki/X-Forwarded-For – Matthew Kolb Dec 31 '14 at 18:45
  • 2
    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. – Rob Brandt May 09 '15 at 21:22
23

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 ();

?>
Erki Aring
  • 2,032
  • 13
  • 15
  • 3
    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. – Ben Dyer Dec 12 '17 at 19:01
  • @BenDyer Thanks, fixed it. – Erki Aring Dec 18 '17 at 14:29
  • 2
    I believe this is incorrect. According to Mozilla, the original IP is the first, not the last, so it should be `trim(array_shift())` https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For – Lucas Bustamante Feb 24 '20 at 12:52
  • 1
    @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 Jan 09 '23 at 19:56
2

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
zeroimpl
  • 2,746
  • 22
  • 19
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

mowgli
  • 2,796
  • 3
  • 31
  • 68
0

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 '';
}
Ensai Tankado
  • 189
  • 1
  • 4
-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.

Community
  • 1
  • 1
TheEnvironmentalist
  • 2,694
  • 2
  • 19
  • 46
  • 8
    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). – BenMorel Sep 29 '13 at 14:28
  • 1
    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. – TheEnvironmentalist Sep 29 '13 at 19:37
  • 7
    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 Jun 26 '15 at 13:04
  • 1
    @transistor09 Care to provide your own answer then? – TheEnvironmentalist Jul 02 '15 at 01:55
  • 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. – CragMonkey Jan 13 '22 at 13:54