DEV Community

Rogerio Taques
Rogerio Taques

Posted on

An easy way to get the (real) client IP in PHP

Here we go with another post! 🤘

In the other day I noticed all IPs recorded from visitor on one of my apps were the same, with a very little variation each-other! Aha~ found a bug!

Well, not much! 🙄 After a quick Google search I figured out that those IPs were all from CloudFlare (the CDN provider my apps are behind)! So, CloudFlare replaces the commonly used $_SERVER['REMOTE_ADDR'] variable with their own IP.

Fair enough!

So, if you're reading this, the chances are you are facing a similar problem and are looking for a solution ... or perhaps you're just curious, which is fine too. 🤪

Here's a very simple way to get the (real) client IP address. Note that the sintax used here is PHP7+, which means for earlier versions you're gonna need to apply some chained ifs and elses.

<?php

# PHP7+
$clientIP = $_SERVER['HTTP_CLIENT_IP'] 
    ?? $_SERVER["HTTP_CF_CONNECTING_IP"] # when behind cloudflare
    ?? $_SERVER['HTTP_X_FORWARDED'] 
    ?? $_SERVER['HTTP_X_FORWARDED_FOR'] 
    ?? $_SERVER['HTTP_FORWARDED'] 
    ?? $_SERVER['HTTP_FORWARDED_FOR'] 
    ?? $_SERVER['REMOTE_ADDR'] 
    ?? '0.0.0.0';

# Earlier than PHP7
$clientIP = '0.0.0.0';

if (isset($_SERVER['HTTP_CLIENT_IP'])) {
    $clientIP = $_SERVER['HTTP_CLIENT_IP'];
} elseif (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
    # when behind cloudflare
    $clientIP = $_SERVER['HTTP_CF_CONNECTING_IP']; 
} elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $clientIP = $_SERVER['HTTP_X_FORWARDED_FOR'];
} elseif (isset($_SERVER['HTTP_X_FORWARDED'])) {
    $clientIP = $_SERVER['HTTP_X_FORWARDED'];
} elseif (isset($_SERVER['HTTP_FORWARDED_FOR'])) {
    $clientIP = $_SERVER['HTTP_FORWARDED_FOR'];
} elseif (isset($_SERVER['HTTP_FORWARDED'])) {
    $clientIP = $_SERVER['HTTP_FORWARDED'];
} elseif (isset($_SERVER['REMOTE_ADDR'])) {
    $clientIP = $_SERVER['REMOTE_ADDR'];
}

echo "My client IP: ", $clientIP;

Enter fullscreen mode Exit fullscreen mode

Happy coding! 👋

Top comments (8)

Collapse
 
arvin profile image
Arvin •

Good post Rogerio :)

I also had to unmask the real IP when I started using Cloudflare.
Since my projects were hosted on my own server and it was more than one project, I decided to use mod_remoteip instead of tweaking the code everywhere, and already get the real IP when I’m using $_SERVER[‘REMOTE_ADDR’].

More on this for those who are in the same situation: support.cloudflare.com/hc/en-us/ar...

Collapse
 
rogeriotaques profile image
Rogerio Taques •

That’s an awesome solution, Arvin! Thanks for sharing. 🙏👏🤩

Collapse
 
boppy profile image
Henning Bopp •

NEVER (like in never-never-ever) use ANY $_SERVER-var that starts with HTTP_ without validating! Vars starting with HTTP_ can be set by any user as header to a request!

Like:

curl -H "client-ip: 10.13.37.42" https://example.com/
Enter fullscreen mode Exit fullscreen mode

Therefore you need to check against your known prox(y|ies). Like:

<?php
$ip = $_SERVER['REMOTE_ADDR'];

// Where 10.42.13.37 is you trusted proxy
if($ip === '10.42.13.37' && isset($_SERVER['HTTP_CLIENT_IP'])){
    // This "fixes" the REMOTE_ADDR field so you do not need
    // to change every access to it...
    $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CLIENT_IP'];
}
Enter fullscreen mode Exit fullscreen mode

CloudFlare replaces the commonly used $_SERVER['REMOTE_ADDR'] variable with their own IP.

It's not a thing CF could change, because your webserver answers to a TCP-request made by the CF jumphost, so your webserver has to send data back through the jumphost that forwards it to your user.

Collapse
 
ms1985 profile image
Mario Stähli •

The danger: IP spoofing (pretending to use fake IP addresses)

All values ​​in PHP that begin with $SERVER['HTTP...'] are simply HTTP headers that the client (i.e., the web browser or an attacker) sends to your server. They can be manipulated at will.

The solution (the "Trusted Proxy" principle):

The only variable in PHP that cannot be spoofed is $_SERVER['REMOTE_ADDR']. This is the physical IP address of the direct TCP connection to your web server.

You may only read and trust headers like HTTP_X_FORWARDED_FOR or HTTP_CF_CONNECTING_IP if you have previously proven that the direct caller (REMOTE_ADDR) is a trusted proxy (load balancer, Cloudflare server) that has set these headers correctly and securely.

function getUserIP(): string {
    $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';

    if (!filter_var($remoteAddr, FILTER_VALIDATE_IP)) {
        return '0.0.0.0';
    }

    if (!isCloudflareIP($remoteAddr)) {
        return $remoteAddr;
    }

    $cfIp = trim((string) ($_SERVER['HTTP_CF_CONNECTING_IP'] ?? ''));

    if (!filter_var($cfIp, FILTER_VALIDATE_IP)) {
        return $remoteAddr;
    }

    return $cfIp;
}

function isCloudflareIP(string $ip): bool {
    static $ranges = null;

    if ($ranges === null) {

        $file = __DIR__ . '/../../cache/cloudflare_ips.json';

        if (!is_file($file)) {
            return false;
        }

        $data = json_decode(file_get_contents($file), true);

        if (!is_array($data)) {
            return false;
        }

        $ranges = prepareCidrs($data);
    }

    return ipInPreparedCidrs($ip, $ranges);
}

function prepareCidrs(array $cidrs): array {
    $prepared = [];

    foreach ($cidrs as $cidr) {

        [$ip, $prefix] = explode('/', $cidr, 2);
        $prefix = (int) $prefix;

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {

            $ipLong = ip2long($ip);
            $mask = -1 << (32 - $prefix);

            $prepared[] = [
                'type' => 4,
                'subnet' => $ipLong & $mask,
                'mask' => $mask
            ];
        } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {

            $prepared[] = [
                'type' => 6,
                'subnet' => inet_pton($ip),
                'prefix' => $prefix
            ];
        }
    }

    return $prepared;
}

function ipInPreparedCidrs(string $ip, array $ranges): bool {
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {

        $ipLong = ip2long($ip);

        foreach ($ranges as $range) {

            if ($range['type'] !== 4) {
                continue;
            }

            if (($ipLong & $range['mask']) === $range['subnet']) {
                return true;
            }
        }

        return false;
    }

    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {

        $ipBin = inet_pton($ip);

        foreach ($ranges as $range) {

            if ($range['type'] !== 6) {
                continue;
            }

            $bytes = intdiv($range['prefix'], 8);
            $bits = $range['prefix'] % 8;

            if ($bytes && substr($ipBin, 0, $bytes) !== substr($range['subnet'], 0, $bytes)) {
                continue;
            }

            if ($bits) {
                $mask = (0xFF << (8 - $bits)) & 0xFF;

                if ((ord($ipBin[$bytes]) & $mask) !== (ord($range['subnet'][$bytes]) & $mask)) {
                    continue;
                }
            }

            return true;
        }
    }

    return false;
}
Enter fullscreen mode Exit fullscreen mode

I obtain the Cloudflare IP addresses via
cloudflare.com/ips-v4 and cloudflare.com/ips-v6.
I have these saved to a JSON file once a day using a cron job.

Collapse
 
tbwcjw profile image
tbwcjw • • Edited

This is not a very clean solution. If Else statements are clunky. Here's my version.

function getClientIP(): string {
    $ipHeaders = [
        'HTTP_CLIENT_IP',
        'HTTP_CF_CONNECTING_IP',
        'HTTP_X_FORWARDED_FOR',
        'HTTP_X_FORWARDED',
        'HTTP_FORWARDED_FOR',
        'HTTP_FORWARDED',
        'REMOTE_ADDR'
    ];

    foreach ($ipHeaders as $header) {
        if (!empty($_SERVER[$header])) {
            return $_SERVER[$header];
        }
    }

    return '0.0.0.0';
}

$clientIP = getClientIP();
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jboada profile image
jboada •

Hi Rogerio,

Awesome solution!

Collapse
 
rileyjones profile image
riley jones • • Edited

I had the same problem. I used the "REMOTE_ADDR" method on my website and it was showing up my cloudflare ip address. I wanted to echo the users ip, so this answer helped me out a ton.

Collapse
 
thefrosty profile image
Austin Passy •

I'm going to use this with Symfony's HTTP Foundation like so:

use Symfony\Component\HttpFoundation\Request;

$request ??= Request::createFromGlobals();
$ip = $request->server->get(
            'HTTP_CLIENT_IP',
            $request->server->get(
                'HTTP_CF_CONNECTING_IP',
                $request->server->get(
                    'HTTP_X_FORWARDED',
                    $request->server->get(
                        'HTTP_X_FORWARDED_FOR',
                        $request->server->get(
                            'HTTP_FORWARDED',
                            $request->server->get(
                                'HTTP_FORWARDED_FOR',
                                $request->server->get('REMOTE_ADDR')
                            )
                        )
                    )
                )
            )
        );
Enter fullscreen mode Exit fullscreen mode