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