324

I know there are a plethora of $_SERVER variables headers available for IP address retrieval. I was wondering if there is a general consensus as to how to most accurately retrieve a user's real IP address (well knowing no method is perfect) using said variables?

I spent some time trying to find an in depth solution and came up with the following code based on a number of sources. I would love it if somebody could please poke holes in the answer or shed some light on something perhaps more accurate.

edit includes optimizations from @Alix

 /**
  * Retrieves the best guess of the client's actual IP address.
  * Takes into account numerous HTTP proxy headers due to variations
  * in how different ISPs handle IP addresses in headers between hops.
  */
 public function get_ip_address() {
  // Check for shared internet/ISP IP
  if (!empty($_SERVER['HTTP_CLIENT_IP']) && $this->validate_ip($_SERVER['HTTP_CLIENT_IP']))
   return $_SERVER['HTTP_CLIENT_IP'];

  // Check for IPs passing through proxies
  if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
   // Check if multiple IP addresses exist in var
    $iplist = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
    foreach ($iplist as $ip) {
     if ($this->validate_ip($ip))
      return $ip;
    }
   }
  }
  if (!empty($_SERVER['HTTP_X_FORWARDED']) && $this->validate_ip($_SERVER['HTTP_X_FORWARDED']))
   return $_SERVER['HTTP_X_FORWARDED'];
  if (!empty($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']) && $this->validate_ip($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']))
   return $_SERVER['HTTP_X_CLUSTER_CLIENT_IP'];
  if (!empty($_SERVER['HTTP_FORWARDED_FOR']) && $this->validate_ip($_SERVER['HTTP_FORWARDED_FOR']))
   return $_SERVER['HTTP_FORWARDED_FOR'];
  if (!empty($_SERVER['HTTP_FORWARDED']) && $this->validate_ip($_SERVER['HTTP_FORWARDED']))
   return $_SERVER['HTTP_FORWARDED'];

  // Return unreliable IP address since all else failed
  return $_SERVER['REMOTE_ADDR'];
 }

 /**
  * Ensures an IP address is both a valid IP address and does not fall within
  * a private network range.
  *
  * @access public
  * @param string $ip
  */
 public function validate_ip($ip) {
     if (filter_var($ip, FILTER_VALIDATE_IP, 
                         FILTER_FLAG_IPV4 | 
                         FILTER_FLAG_IPV6 |
                         FILTER_FLAG_NO_PRIV_RANGE | 
                         FILTER_FLAG_NO_RES_RANGE) === false)
         return false;
     self::$ip = $ip;
     return true;
 }

Words of Warning (update)

REMOTE_ADDR still represents the most reliable source of an IP address. The other $_SERVER variables mentioned here can be spoofed by a remote client very easily. The purpose of this solution is to attempt to determine the IP address of a client sitting behind a proxy. For your general purposes, you might consider using this in combination with the IP address returned directly from $_SERVER['REMOTE_ADDR'] and storing both.

For 99.9% of users this solution will suit your needs perfectly. It will not protect you from the 0.1% of malicious users looking to abuse your system by injecting their own request headers. If relying on IP addresses for something mission critical, resort to REMOTE_ADDR and don't bother catering to those behind a proxy.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Corey Ballou
  • 42,389
  • 8
  • 62
  • 75
  • I tried these variables and they were all blank except $_SERVER['REMOTE_ADDR'], which gave me a private IP address. Does anyone know how they do it at whatismyip.com? On that site, I get a useful IP address. – Elias Zamaria Nov 02 '09 at 18:57
  • As chisum commented below, the majority of of $_SERVER variables related to the user's IP address are merely optional as your packets make their way across the web. I would recommend doing a var_dump() of your $_SERVER variable to see what comes up for you. Regarding how whatismyip.com obtains your IP address, I wish I knew the answer. – Corey Ballou Nov 03 '09 at 11:28
  • 2
    For the whatismyip.com question, I am thinking they do something like this script, are you running it locally? If you are thats why you have an internal IP, nothing is getting sent over the public interface in that case so there is no information for php to get – Matt Jan 08 '10 at 18:00
  • 2
    Make sure you keep this in mind when implementing this: http://stackoverflow.com/questions/1672827/php-ajax-remoteaddr-set-to-ip-of-bogus-network-adapter/1678748#1678748 – Kevin Peno Jan 08 '10 at 19:09
  • 19
    Remember that all these HTTP headers are really easy to modify : with your solution, I just have to configure my browser to send a X-Forwarded-For header with a random IP and your script will happily return a fake address. So depending on what you are trying to do, this solution could be less reliable than simply using REMOTE_ADDR. – gnomnain Jan 09 '10 at 15:00
  • @cballou: Check my edit for a cleaner version of the code. – Alix Axel Jan 10 '10 at 01:05
  • You seem to have an extra '}' in get_ip_address. Could you post the entire class? This code does not work outside of a class. It would be nice for the code to be ready for copy-and-paste. Alternately, see my answer below. – Joseph Turian Jan 25 '10 at 01:32
  • 17
    OMFG, "unreliable ip"! First time I see such a nonsense here on SO. The only reliable IP address is REMOTE_ADDR – Your Common Sense Mar 24 '10 at 12:44
  • 5
    -1 this is vulnerable to spoofing. All you are doing is asking the user what his ip address should be. – rook Jun 24 '11 at 23:36
  • @Rook: What if the OP wants to provide a non-security related service, like whatismyip.com? – Alix Axel Jun 25 '11 at 01:49
  • @cballou - Just wanted to let you know that the validate_ip function is broken because it's testing for both FILTER_FLAG_IPV4 and FILTER_FLAG_IPV6 and when you have both in it it always returns false (try it, you'll see that every IP you put into it returns false). If you remove the FILTER_FLAG_IPV6 it'll work correctly. I assume you added in Alix's suggestion of checking for IP6 and didn't realize having both those flags in it at once breaks it. – Tim Feb 01 '12 at 05:21
  • I've added an example to [documentation](http://stackoverflow.com/documentation/php/5058/how-to-detect-client-ip-address#t=20160803124653603355), that demonstrates basic security checks needed when using `HTTP_X_FORWARDED_FOR` – Erki Aring Aug 03 '16 at 12:55
  • 1
    there is a unnecessary closing } on line 19. Can't edit post as it's only 1 character – LaCorb Jun 11 '20 at 10:06
  • You should not be trusting the X-Forwarded-For header unless (at least) the connection (REMOTE_ADDR) comes from a trusted set of IP addresses (your load balancers, or 127.0.0.1/::1 if you're using nginx/haproxy locally). – sromero Aug 11 '21 at 13:28
  • Do keep in mind that REMOTE_ADDR is still potentially spoofed. See https://spoofer.caida.org/summary.php – Dalton Bentley Jun 06 '23 at 14:39

19 Answers19

300

Here is a shorter, cleaner way to get the IP address:

function get_ip_address(){
    foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR') as $key){
        if (array_key_exists($key, $_SERVER) === true){
            foreach (explode(',', $_SERVER[$key]) as $ip){
                $ip = trim($ip); // just to be safe

                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false){
                    return $ip;
                }
            }
        }
    }
}

Your code seems to be pretty complete already, I cannot see any possible bugs in it (aside from the usual IP caveats), I would change the validate_ip() function to rely on the filter extension though:

public function validate_ip($ip)
{
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false)
    {
        return false;
    }

    self::$ip = sprintf('%u', ip2long($ip)); // you seem to want this

    return true;
}

Also your HTTP_X_FORWARDED_FOR snippet can be simplified from this:

// check for IPs passing through proxies
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
{
    // check if multiple ips exist in var
    if (strpos($_SERVER['HTTP_X_FORWARDED_FOR'], ',') !== false)
    {
        $iplist = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        
        foreach ($iplist as $ip)
        {
            if ($this->validate_ip($ip))
                return $ip;
        }
    }
    
    else
    {
        if ($this->validate_ip($_SERVER['HTTP_X_FORWARDED_FOR']))
            return $_SERVER['HTTP_X_FORWARDED_FOR'];
    }
}

To this:

// check for IPs passing through proxies
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
{
    $iplist = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        
    foreach ($iplist as $ip)
    {
        if ($this->validate_ip($ip))
            return $ip;
    }
}

You may also want to validate IPv6 addresses.

Dharman
  • 30,962
  • 25
  • 85
  • 135
Alix Axel
  • 151,645
  • 95
  • 393
  • 500
  • 4
    I definitely appreciate the `filter_var` fix as it removes a bunch of hackish unsigned int checks on the IP address. I also like the fact it gives me the option of validating IPv6 addresses as well. The `HTTP_X_FORWARDED_FOR` optimization is also much appreciated. In a few minutes I will update the code. – Corey Ballou Jan 09 '10 at 13:37
  • filter_var didn't work for me until I fixed it by adding || between options params instead of | .. Here is what made it work: FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 || FILTER_FLAG_IPV6 || FILTER_FLAG_NO_PRIV_RANGE || FILTER_FLAG_NO_RES_RANGE – Alex L Feb 17 '10 at 02:10
  • It's worth noting that usage of filter_var will only work with PHP > 5.2 – Corey Ballou Mar 16 '10 at 17:46
  • `if (array_key_exists($key, $_SERVER) === true) { }` could be shortened to `if (array_key_exists($key, $_SERVER)) { }`, since array_key_exists always returns `true` or `false`. – Mathias Bynens Mar 24 '10 at 06:21
  • @Mathias Bynens: Yes, I know. My coding style is way too explicit sometimes, old habits die hard. =\ – Alix Axel Mar 25 '10 at 04:49
  • @alix well I assume you fail to confirm practical use of your code :) – Your Common Sense Mar 26 '10 at 17:11
  • @Col. Shrapnel: You assume right, I never had the need to use anything other than `REMOTE_ADDR`, still I think you should pay some attention where I mention the usual IP caveats of relying on more than the `REMOTE_ADDR` index **and** the OP original revision (http://stackoverflow.com/revisions/e733f4af-69c0-40a4-b168-3a5b2782d92a/view-source). You arrived a couple of months late to this discussion buddy. – Alix Axel Mar 27 '10 at 12:55
  • For me, filter_var had to be formatted thusly before it would work: filter_var($ip, FILTER_VALIDATE_IP, array('flags' => 'FILTER_FLAG_IPV4|FILTER_FLAG_IPV6|FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE')) – vamin May 16 '10 at 21:45
  • 37
    -1 this is vulnerable to spoofing all you are doing is asking the user what his ip address should be. – rook Jun 24 '11 at 23:38
  • 13
    @Rook: Yes, I know. The OP is aware of that, and I've also mentioned it in my answer. But thanks for the comment. – Alix Axel Jun 25 '11 at 01:45
  • 1
    FYI: I had to remove FILTER_FLAG_IPV6 to get Alix Axel's code to work. – darkAsPitch Sep 27 '11 at 08:41
  • 1
    Can you explain what exactly the line self::$ip = $ip; is for? That seems to be giving me errors but works great if I remove it? – darkAsPitch Sep 27 '11 at 08:51
  • @darkAsPitch: It was part of the OP question / code (a class property). To use it standalone `self::$ip = $ip; return $ip;` should become `return $ip;`. – Alix Axel Sep 27 '11 at 08:56
  • I think you must put some things into a variabele first before start to loop and i think using isset instead of in_array is also a speed improvement. – Codebeat Feb 21 '12 at 02:55
  • @AlixAxel I tried running this script but I'm not getting a result, just a blank page. – Scarl Jul 03 '15 at 22:32
  • In first function need add " return $ip; " before last } –  Jan 22 '16 at 21:05
  • @rook that is the only way to do that, or you have another manner? explain please. –  Jan 22 '16 at 21:12
  • 2
    @rubenrp81 The TCP socket handler is the only canonical source, everything else is attacker-controlled. The code above is an attacker's dream. – rook Feb 01 '16 at 11:20
  • You can only trust mentioned HTTP_* vars when the proxy is specifically configured to handle these variables in secure manner and you check that the request is coming from trusted proxy. None of these checks are present in the above code, so this is a big upvoted security hole. – Erki Aring Aug 03 '16 at 07:32
  • 1
    Wouldn't this solution totally miss the ball on CLOUD FLARE? Should not we account for "HTTP_CF_CONNECTING_IP" ? https://support.cloudflare.com/hc/en-us/articles/200170876-I-can-t-install-mod-cloudflare-and-there-s-no-plugin-to-restore-original-visitor-IP-What-should-I-do- – Average Joe Feb 27 '17 at 13:59
  • This doesn't work for php < 7 version. Please update your answer ) – Gag Baghdasaryan Feb 07 '20 at 11:18
13

Even then however, getting a user's real IP address is going to be unreliable. All they need to do is use an anonymous proxy server (one that doesn't honor the headers for http_x_forwarded_for, http_forwarded, etc) and all you get is their proxy server's IP address.

You can then see if there is a list of proxy server IP addresses that are anonymous, but there is no way to be sure that is 100% accurate as well and the most it'd do is let you know it is a proxy server. And if someone is being clever, they can spoof headers for HTTP forwards.

Let's say I don't like the local college. I figure out what IP addresses they registered, and get their IP address banned on your site by doing bad things, because I figure out you honor the HTTP forwards. The list is endless.

Then there is, as you guessed, internal IP addresses such as the college network I metioned before. A lot use a 10.x.x.x format. So all you would know is that it was forwarded for a shared network.

Then I won't start much into it, but dynamic IP addresses are the way of broadband anymore. So. Even if you get a user IP address, expect it to change in 2 - 3 months, at the longest.

Jenia
  • 374
  • 1
  • 4
  • 15
  • Thanks for the input. I'm currently utilizing the user's IP address to aid in session authentication by using their class C IP as a limiting factor to limit session hijacking but allow for dynamic IPs within reason. Spoofed IPs and anonymous proxy servers is just something I'll have to deal with for a select group of individuals. – Corey Ballou Oct 28 '09 at 10:04
  • @cballou - Surely for this purpose REMOTE_ADDR is the correct one to use. Any approach relying on HTTP headers is vulnerable to header spoofing. How long is a session? Dynamic IPs don't change fast. – MZB Jan 12 '10 at 13:38
  • They do, especially if I want them to (change mac address which many drivers do support). Just REMOTE_ADDR by itself is enough to get what ever the last server it talked to was. So in a proxy situation you get the proxy IP. –  Jan 23 '10 at 00:33
9

We use:

/**
 * Get the customer's IP address.
 *
 * @return string
 */
public function getIpAddress() {
    if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
        return $_SERVER['HTTP_CLIENT_IP'];
    } else if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        return trim($ips[count($ips) - 1]);
    } else {
        return $_SERVER['REMOTE_ADDR'];
    }
}

The explode on HTTP_X_FORWARDED_FOR is because of weird issues we had detecting IP addresses when Squid was used.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
gabrielk
  • 573
  • 6
  • 11
  • Whoops, I just realized you do basically the same thing with exploding on , and so forth. Plus a little extra. So I doubt my answer was much help. :) – gabrielk Jan 07 '10 at 20:45
  • 1
    This returns the localhost's address – Scarl Jul 03 '15 at 22:34
5

My answer is basically just a polished, fully-validated, and fully-packaged, version of @AlixAxel's answer:

<?php

/* Get the 'best known' client IP. */

if (!function_exists('getClientIP'))
    {
        function getClientIP()
            {
                if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) 
                    {
                        $_SERVER['REMOTE_ADDR'] = $_SERVER["HTTP_CF_CONNECTING_IP"];
                    };

                foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR') as $key)
                    {
                        if (array_key_exists($key, $_SERVER)) 
                            {
                                foreach (explode(',', $_SERVER[$key]) as $ip)
                                    {
                                        $ip = trim($ip);

                                        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false)
                                            {
                                                return $ip;
                                            };
                                    };
                            };
                    };

                return false;
            };
    };

$best_known_ip = getClientIP();

if(!empty($best_known_ip))
    {
        $ip = $clients_ip = $client_ip = $client_IP = $best_known_ip;
    }
else
    {
        $ip = $clients_ip = $client_ip = $client_IP = $best_known_ip = '';
    };

?>

Changes:

  • It simplifies the function name (with 'camelCase' formatting style).

  • It includes a check to make sure the function isn't already declared in another part of your code.

  • It takes into account 'CloudFlare' compatibility.

  • It initializes multiple "IP-related" variable names to the returned value, of the 'getClientIP' function.

  • It ensures that if the function doesn't return a valid IP address, all the variables are set to a empty string, instead of null.

  • It's only (45) lines of code.

James Anderson Jr.
  • 760
  • 1
  • 8
  • 26
3

The biggest question is for what purpose?

Your code is nearly as comprehensive as it could be - but I see that if you spot what looks like a proxy added header, you use that INSTEAD of the CLIENT_IP, however if you want this information for audit purposes then be warned - its very easy to fake.

Certainly you should never use IP addresses for any sort of authentication - even these can be spoofed.

You could get a better measurement of the client ip address by pushing out a flash or java applet which connects back to the server via a non-http port (which would therefore reveal transparent proxies or cases where the proxy-injected headers are false - but bear in mind that, where the client can ONLY connect via a web proxy or the outgoing port is blocked, there will be no connection from the applet.

Dharman
  • 30,962
  • 25
  • 85
  • 135
symcbean
  • 47,736
  • 6
  • 59
  • 94
  • Taking into consideration I am looking for a PHP only solution, are you suggesting I add `$_SERVER['CLIENT_IP']` as the second else if statement? – Corey Ballou Jan 11 '10 at 12:03
  • No - just that if you want to place any significance on the data returned, then it would be a good idea to preserve the network end point address (client IP) as well as anything suggesting a different value in proxy added headers (e.g. you may see lots of 192.168.1.x addresses but coming from different client ips) C. – symcbean Jan 11 '10 at 12:16
1

Thanks for this, very useful.

It would help though if the code were syntactically correct. As it is there's a { too many around line 20. Which I'm afraid means nobody actually tried this out.

I may be crazy, but after trying it on a few valid and invalid addresses, the only version of validate_ip() that worked was this:

    public function validate_ip($ip)
    {
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) === false)
            return false;
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) === false)
            return false;
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false)
            return false;

        return true;
    }
Mark Boon
  • 113
  • 2
  • 9
1

Here's a modified version if you use CloudFlare caching layer Services

function getIP()
{
    $fields = array('HTTP_X_FORWARDED_FOR',
                    'REMOTE_ADDR',
                    'HTTP_CF_CONNECTING_IP',
                    'HTTP_X_CLUSTER_CLIENT_IP');

    foreach($fields as $f)
    {
        $tries = $_SERVER[$f];
        if (empty($tries))
            continue;
        $tries = explode(',',$tries);
        foreach($tries as $try)
        {
            $r = filter_var($try,
                            FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 |
                            FILTER_FLAG_NO_PRIV_RANGE |
                            FILTER_FLAG_NO_RES_RANGE);

            if ($r !== false)
            {
                return $try;
            }
        }
    }
    return false;
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
jmserra
  • 1,296
  • 4
  • 18
  • 34
1

Just another clean way:

  function validateIp($var_ip){
    $ip = trim($var_ip);

    return (!empty($ip) &&
            $ip != '::1' &&
            $ip != '127.0.0.1' &&
            filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false)
            ? $ip : false;
  }

  function getClientIp() {
    $ip = @$this->validateIp($_SERVER['HTTP_CLIENT_IP']) ?:
          @$this->validateIp($_SERVER['HTTP_X_FORWARDED_FOR']) ?:
          @$this->validateIp($_SERVER['HTTP_X_FORWARDED']) ?:
          @$this->validateIp($_SERVER['HTTP_FORWARDED_FOR']) ?:
          @$this->validateIp($_SERVER['HTTP_FORWARDED']) ?:
          @$this->validateIp($_SERVER['REMOTE_ADDR']) ?:
          'LOCAL OR UNKNOWN ACCESS';

    return $ip;
  }
Liko
  • 2,130
  • 19
  • 20
1

i realize there are much better and more concise answers above, and this isnt a function nor the most graceful script around. In our case we needed to output both the spoofable x_forwarded_for and the more reliable remote_addr in a simplistic switch per-say. It needed to allow blanks for injecting into other functions if-none or if-singular (rather than just returning the preformatted function). It needed an "on or off" var with a per-switch customized label(s) for platform settings. It also needed a way for $ip to be dynamic depending on request so that it would take form of forwarded_for.

Also i didnt see anyone address isset() vs !empty() -- its possible to enter nothing for x_forwarded_for yet still trigger isset() truth resulting in blank var, a way to get around is to use && and combine both as conditions. Keep in mind you can spoof words like "PWNED" as x_forwarded_for so make sure you sterilize to a real ip syntax if your outputting somewhere protected or into DB.

Also also, you can test using google translate if you need a multi-proxy to see the array in x_forwarder_for. If you wanna spoof headers to test, check this out Chrome Client Header Spoof extension. This will default to just standard remote_addr while behind anon proxy.

I dunno any case where remote_addr could be empty, but its there as fallback just in case.

// proxybuster - attempts to un-hide originating IP if [reverse]proxy provides methods to do so
  $enableProxyBust = true;

if (($enableProxyBust == true) && (isset($_SERVER['REMOTE_ADDR'])) && (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) && (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))) {
    $ip = end(array_values(array_filter(explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']))));
    $ipProxy = $_SERVER['REMOTE_ADDR'];
    $ipProxy_label = ' behind proxy ';
} elseif (($enableProxyBust == true) && (isset($_SERVER['REMOTE_ADDR']))) {
    $ip = $_SERVER['REMOTE_ADDR'];
    $ipProxy = '';
    $ipProxy_label = ' no proxy ';
} elseif (($enableProxyBust == false) && (isset($_SERVER['REMOTE_ADDR']))) {
    $ip = $_SERVER['REMOTE_ADDR'];
    $ipProxy = '';
    $ipProxy_label = '';
} else {
    $ip = '';
    $ipProxy = '';
    $ipProxy_label = '';
}

To make these dynamic for use in function(s) or query/echo/views below, say for log gen or error reporting, use globals or just echo em in wherever you desire without making a ton of other conditions or static-schema-output functions.

function fooNow() {
    global $ip, $ipProxy, $ipProxy_label;
    // begin this actions such as log, error, query, or report
}

Thank you for all your great thoughts. Please let me know if this could be better, still kinda new to these headers :)

dhaupin
  • 1,613
  • 2
  • 21
  • 24
1

I came up with this function that does not simply return the IP address but an array with IP information.

// Example usage:
$info = ip_info();
if ( $info->proxy ) {
    echo 'Your IP is ' . $info->ip;
} else {
    echo 'Your IP is ' . $info->ip . ' and your proxy is ' . $info->proxy_ip;
}

Here's the function:

/**
 * Retrieves the best guess of the client's actual IP address.
 * Takes into account numerous HTTP proxy headers due to variations
 * in how different ISPs handle IP addresses in headers between hops.
 *
 * @since 1.1.3
 *
 * @return object {
 *         IP Address details
 *
 *         string $ip The users IP address (might be spoofed, if $proxy is true)
 *         bool $proxy True, if a proxy was detected
 *         string $proxy_id The proxy-server IP address
 * }
 */
function ip_info() {
    $result = (object) array(
        'ip' => $_SERVER['REMOTE_ADDR'],
        'proxy' => false,
        'proxy_ip' => '',
    );

    /*
     * This code tries to bypass a proxy and get the actual IP address of
     * the visitor behind the proxy.
     * Warning: These values might be spoofed!
     */
    $ip_fields = array(
        'HTTP_CLIENT_IP',
        'HTTP_X_FORWARDED_FOR',
        'HTTP_X_FORWARDED',
        'HTTP_X_CLUSTER_CLIENT_IP',
        'HTTP_FORWARDED_FOR',
        'HTTP_FORWARDED',
        'REMOTE_ADDR',
    );
    foreach ( $ip_fields as $key ) {
        if ( array_key_exists( $key, $_SERVER ) === true ) {
            foreach ( explode( ',', $_SERVER[$key] ) as $ip ) {
                $ip = trim( $ip );

                if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) !== false ) {
                    $forwarded = $ip;
                    break 2;
                }
            }
        }
    }

    // If we found a different IP address then REMOTE_ADDR then it's a proxy!
    if ( $forwarded != $result->ip ) {
        $result->proxy = true;
        $result->proxy_ip = $result->ip;
        $result->ip = $forwarded;
    }

    return $result;
}
Philipp
  • 10,240
  • 8
  • 59
  • 71
1

As someone said previously, the key here is for what reason you want to store user's ips.

I'll give an example from a registration system I work on and of course the solution just to contribute sth in this old discussion that comes frequently in my searches.

Many php registration libraries use ip to throttle/lock out failed attempts based on user's ip. Consider this table:

-- mysql
DROP TABLE IF EXISTS `attempts`;
CREATE TABLE `attempts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `ip` varchar(39) NOT NULL, /*<<=====*/
  `expiredate` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 -- sqlite
...

Then, when a user tries to do a login or anything related with servicing like a password reset, a function is called at the start:

public function isBlocked() {
      /*
       * used one of the above methods to capture user's ip!!!
       */
      $ip = $this->ip;
      // delete attempts from this ip with 'expiredate' in the past
      $this->deleteAttempts($ip, false);
      $query = $this->dbh->prepare("SELECT count(*) FROM {$this->token->get('table_attempts')} WHERE ip = ?");
      $query->execute(array($ip));
      $attempts = $query->fetchColumn();
      if ($attempts < intval($this->token->get('attempts_before_verify'))) {
         return "allow";
      }
      if ($attempts < intval($this->token->get('attempts_before_ban'))) {
         return "captcha";
      }
      return "block";
   }

Say, for example, $this->token->get('attempts_before_ban') === 10 and 2 users come for the same ips as is the case in the previous codes where headers can be spoofed, then after 5 attempts each both are banned! Even worst, if all come from the same proxy then only the first 10 users will be logged and all the rest will be banned!

The critical here is that we need a unique index on table attempts and we can get it from a combination like:

 `ip` varchar(39) NOT NULL,
 `jwt_load varchar(100) NOT NULL

where jwt_load comes from a http cookie that follows the json web token technology where we store only the encrypted payload that should contain an arbitrary/unique value for every user. Of course the request should be modified to: "SELECT count(*) FROM {$this->token->get('table_attempts')} WHERE ip = ? AND jwt_load = ?" and the class should also initiate a private $jwt.

centurian
  • 1,168
  • 13
  • 25
0

I do wonder if perhaps you should iterate over the exploded HTTP_X_FORWARDED_FOR in reverse order, since my experience has been that the user's IP address ends up at the end of the comma-separated list, so starting at the start of the header, you're more likely to get the ip address of one of the proxies returned, which could potentially still allow session hijacking as many users may come through that proxy.

Chris Withers
  • 10,837
  • 4
  • 33
  • 51
  • 1
    Having read the wikipedia page on HTTP_X_FORWARDED_FOR: http://en.wikipedia.org/wiki/X-Forwarded-For ...I see the suggested order is, indeed, left to right as your code has it. However, from our logs I can see there are a lot of cases where this is not respected by proxies in the wild and the ip address your want to be checking could be at either end of the list. – Chris Withers Jan 15 '10 at 08:15
  • 1
    Or in the middle, as would happen if some of the proxies respected the left-to-right order and others didn't. – Brilliand May 26 '11 at 20:11
0

From Symfony's Request class https://github.com/symfony/symfony/blob/1bd125ec4a01220878b3dbc3ec3156b073996af9/src/Symfony/Component/HttpFoundation/Request.php

const HEADER_FORWARDED = 'forwarded';
const HEADER_CLIENT_IP = 'client_ip';
const HEADER_CLIENT_HOST = 'client_host';
const HEADER_CLIENT_PROTO = 'client_proto';
const HEADER_CLIENT_PORT = 'client_port';

/**
 * Names for headers that can be trusted when
 * using trusted proxies.
 *
 * The FORWARDED header is the standard as of rfc7239.
 *
 * The other headers are non-standard, but widely used
 * by popular reverse proxies (like Apache mod_proxy or Amazon EC2).
 */
protected static $trustedHeaders = array(
    self::HEADER_FORWARDED => 'FORWARDED',
    self::HEADER_CLIENT_IP => 'X_FORWARDED_FOR',
    self::HEADER_CLIENT_HOST => 'X_FORWARDED_HOST',
    self::HEADER_CLIENT_PROTO => 'X_FORWARDED_PROTO',
    self::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT',
);

/**
 * Returns the client IP addresses.
 *
 * In the returned array the most trusted IP address is first, and the
 * least trusted one last. The "real" client IP address is the last one,
 * but this is also the least trusted one. Trusted proxies are stripped.
 *
 * Use this method carefully; you should use getClientIp() instead.
 *
 * @return array The client IP addresses
 *
 * @see getClientIp()
 */
public function getClientIps()
{
    $clientIps = array();
    $ip = $this->server->get('REMOTE_ADDR');
    if (!$this->isFromTrustedProxy()) {
        return array($ip);
    }
    if (self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) {
        $forwardedHeader = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
        preg_match_all('{(for)=("?\[?)([a-z0-9\.:_\-/]*)}', $forwardedHeader, $matches);
        $clientIps = $matches[3];
    } elseif (self::$trustedHeaders[self::HEADER_CLIENT_IP] && $this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP])) {
        $clientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP])));
    }
    $clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from
    $firstTrustedIp = null;
    foreach ($clientIps as $key => $clientIp) {
        // Remove port (unfortunately, it does happen)
        if (preg_match('{((?:\d+\.){3}\d+)\:\d+}', $clientIp, $match)) {
            $clientIps[$key] = $clientIp = $match[1];
        }
        if (!filter_var($clientIp, FILTER_VALIDATE_IP)) {
            unset($clientIps[$key]);
        }
        if (IpUtils::checkIp($clientIp, self::$trustedProxies)) {
            unset($clientIps[$key]);
            // Fallback to this when the client IP falls into the range of trusted proxies
            if (null ===  $firstTrustedIp) {
                $firstTrustedIp = $clientIp;
            }
        }
    }
    // Now the IP chain contains only untrusted proxies and the client IP
    return $clientIps ? array_reverse($clientIps) : array($firstTrustedIp);
}
luchaninov
  • 6,792
  • 6
  • 60
  • 75
0

I'm surprised no one has mentioned filter_input, so here is Alix Axel's answer condensed to one-line:

function get_ip_address(&$keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'])
{
    return empty($keys) || ($ip = filter_input(INPUT_SERVER, array_pop($keys), FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE))? $ip : get_ip_address($keys);
}
Design.Garden
  • 3,607
  • 25
  • 21
0

I know this is too late to answer. But you may try these options:

Option 1: (Using curl)

$ch = curl_init();

// set URL and other appropriate options
curl_setopt($ch, CURLOPT_URL, "https://ifconfig.me/");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

// grab URL and pass it to the browser
$ip = curl_exec($ch);

// close cURL resource, and free up system resources
curl_close($ch);

return $ip;

Option 2: (Works good on mac)

return trim(shell_exec("dig +short myip.opendns.com @resolver1.opendns.com"));

Option 3: (Just used a trick)

return str_replace('Current IP CheckCurrent IP Address: ', '', strip_tags(file_get_contents('http://checkip.dyndns.com')));

Might be a reference: https://www.tecmint.com/find-linux-server-public-ip-address/

Blues Clues
  • 1,694
  • 3
  • 31
  • 72
0

Though this post very old but the topic still demands attention. So here I come with another solution I used in my project. I found other solutions here either incomplete or too complex to understand.

if (! function_exists('get_visitor_IP'))
{
    /**
     * Get the real IP address from visitors proxy. e.g. Cloudflare
     *
     * @return string IP
     */
    function get_visitor_IP()
    {
        // Get real visitor IP behind CloudFlare network
        if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
            $_SERVER['REMOTE_ADDR'] = $_SERVER["HTTP_CF_CONNECTING_IP"];
            $_SERVER['HTTP_CLIENT_IP'] = $_SERVER["HTTP_CF_CONNECTING_IP"];
        }

        // Sometimes the `HTTP_CLIENT_IP` can be used by proxy servers
        $ip = @$_SERVER['HTTP_CLIENT_IP'];
        if (filter_var($ip, FILTER_VALIDATE_IP)) {
           return $ip;
        }

        // Sometimes the `HTTP_X_FORWARDED_FOR` can contain more than IPs 
        $forward_ips = @$_SERVER['HTTP_X_FORWARDED_FOR'];
        if ($forward_ips) {
            $all_ips = explode(',', $forward_ips);

            foreach ($all_ips as $ip) {
                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)){
                    return $ip;
                }
            }
        }

        return $_SERVER['REMOTE_ADDR'];
    }
}
Munna Khan
  • 1,902
  • 1
  • 18
  • 24
-1

You pretty much answered your own question! :)

function getRealIpAddr() {
    if(!empty($_SERVER['HTTP_CLIENT_IP']))   //Check IP address from shared Internet
    {
        $IPaddress = $_SERVER['HTTP_CLIENT_IP'];
    }
    elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))   //To check IP address is passed from the proxy
    {
        $IPaddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
    }
    else
    {
        $IPaddress = $_SERVER['REMOTE_ADDR'];
    }
    return $IPaddress;
}

Source

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Alex Weber
  • 2,186
  • 2
  • 19
  • 27
-1

Just a VB.NET version of the answer:

Private Function GetRequestIpAddress() As IPAddress
    Dim serverVariables = HttpContext.Current.Request.ServerVariables
    Dim headersKeysToCheck = {"HTTP_CLIENT_IP", _
                              "HTTP_X_FORWARDED_FOR", _
                              "HTTP_X_FORWARDED", _
                              "HTTP_X_CLUSTER_CLIENT_IP", _
                              "HTTP_FORWARDED_FOR", _
                              "HTTP_FORWARDED", _
                              "REMOTE_ADDR"}
    For Each thisHeaderKey In headersKeysToCheck
        Dim thisValue = serverVariables.Item(thisHeaderKey)
        If thisValue IsNot Nothing Then
            Dim validAddress As IPAddress = Nothing
            If IPAddress.TryParse(thisValue, validAddress) Then
                Return validAddress
            End If
        End If
    Next
    Return Nothing
End Function
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Abacus
  • 2,041
  • 1
  • 18
  • 23
-6
/**
 * Sanitizes IPv4 address according to Ilia Alshanetsky's book
 * "php|architect?s Guide to PHP Security", chapter 2, page 67.
 *
 * @param string $ip An IPv4 address
 */
public static function sanitizeIpAddress($ip = '')
{
if ($ip == '')
    {
    $rtnStr = '0.0.0.0';
    }
else
    {
    $rtnStr = long2ip(ip2long($ip));
    }

return $rtnStr;
}

//---------------------------------------------------

/**
 * Returns the sanitized HTTP_X_FORWARDED_FOR server variable.
 *
 */
public static function getXForwardedFor()
{
if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
    {
    $rtnStr = $_SERVER['HTTP_X_FORWARDED_FOR'];
    }
elseif (isset($HTTP_SERVER_VARS['HTTP_X_FORWARDED_FOR']))
    {
    $rtnStr = $HTTP_SERVER_VARS['HTTP_X_FORWARDED_FOR'];
    }
elseif (getenv('HTTP_X_FORWARDED_FOR'))
    {
    $rtnStr = getenv('HTTP_X_FORWARDED_FOR');
    }
else
    {
    $rtnStr = '';
    }

// Sanitize IPv4 address (Ilia Alshanetsky):
if ($rtnStr != '')
    {
    $rtnStr = explode(', ', $rtnStr);
    $rtnStr = self::sanitizeIpAddress($rtnStr[0]);
    }

return $rtnStr;
}

//---------------------------------------------------

/**
 * Returns the sanitized REMOTE_ADDR server variable.
 *
 */
public static function getRemoteAddr()
{
if (isset($_SERVER['REMOTE_ADDR']))
    {
    $rtnStr = $_SERVER['REMOTE_ADDR'];
    }
elseif (isset($HTTP_SERVER_VARS['REMOTE_ADDR']))
    {
    $rtnStr = $HTTP_SERVER_VARS['REMOTE_ADDR'];
    }
elseif (getenv('REMOTE_ADDR'))
    {
    $rtnStr = getenv('REMOTE_ADDR');
    }
else
    {
    $rtnStr = '';
    }

// Sanitize IPv4 address (Ilia Alshanetsky):
if ($rtnStr != '')
    {
    $rtnStr = explode(', ', $rtnStr);
    $rtnStr = self::sanitizeIpAddress($rtnStr[0]);
    }

return $rtnStr;
}

//---------------------------------------------------

/**
 * Returns the sanitized remote user and proxy IP addresses.
 *
 */
public static function getIpAndProxy()
{
$xForwarded = self::getXForwardedFor();
$remoteAddr = self::getRemoteAddr();

if ($xForwarded != '')
    {
    $ip    = $xForwarded;
    $proxy = $remoteAddr;
    }
else
    {
    $ip    = $remoteAddr;
    $proxy = '';
    }

return array($ip, $proxy);
}
Mez
  • 24,430
  • 14
  • 71
  • 93