Database

MySQL statements you'll come back to

HCOMS February 2026 8 min read

Every PHP tutorial tells you the same thing:

$ip = $_SERVER['REMOTE_ADDR'];

This is right, and also wrong. It's right if your PHP server speaks directly to the visitor. It's wrong on basically every modern hosting setup, where there's a Cloudflare, a load balancer, or a reverse proxy between PHP and the public internet — in which case REMOTE_ADDR is the proxy's IP, not the visitor's.

The naive solution (and why it's dangerous)

You'll see this pattern all over Stack Overflow:

function get_ip() {
  if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    return $_SERVER['HTTP_X_FORWARDED_FOR'];
  }
  return $_SERVER['REMOTE_ADDR'];
}

This is an IP spoofing vulnerability. X-Forwarded-For is just an HTTP header — anyone can send any value they like:

curl -H "X-Forwarded-For: 1.2.3.4" https://your-site.example/

Now your access logs, your rate limiting, your IP-based admin restrictions, and your fraud-detection rules all think the request came from 1.2.3.4. If you use that IP for anything that matters, you have a problem.

The right way: trust only your own proxy

The rule is simple: X-Forwarded-For can only be trusted if the immediate previous hop (i.e. REMOTE_ADDR) is your own infrastructure. Anything else is unverified user input.

function get_client_ip(array $trusted_proxies = []): string {
  $remote = $_SERVER['REMOTE_ADDR'] ?? '';

  // If the request didn't come from a trusted proxy,
  // REMOTE_ADDR is the truth.
  if (!in_array($remote, $trusted_proxies, true)) {
    return $remote;
  }

  // It came from our proxy — read the forwarded chain.
  $forwarded = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
  $chain = array_map('trim', explode(',', $forwarded));

  // Walk the chain from the rightmost (closest to us) backwards,
  // skipping any IP we know to be a trusted proxy.
  foreach (array_reverse($chain) as $ip) {
    if (filter_var($ip, FILTER_VALIDATE_IP)
        && !in_array($ip, $trusted_proxies, true)) {
      return $ip;
    }
  }

  return $remote;
}

// Example usage
$ip = get_client_ip([
  '127.0.0.1',
  '10.0.0.1',
  // Cloudflare's published ranges, your load balancer's IPs, etc.
]);

Cloudflare specifically

Cloudflare puts the real client IP in its own header, CF-Connecting-IP, which they sign and which you can trust if (and only if) the request really did come through Cloudflare:

$ip = $_SERVER['HTTP_CF_CONNECTING_IP']
   ?? $_SERVER['REMOTE_ADDR'];

Combined with restricting your origin server's firewall to Cloudflare's IP ranges, this is robust. Cloudflare publishes the current ranges at cloudflare.com/ips.

Laravel users

Frameworks already solve this. In Laravel:

$ip = $request->ip();

Just configure trusted proxies in app/Http/Middleware/TrustProxies.php:

protected $proxies = '*';  // if you trust your hosting environment
protected $headers =
    Request::HEADER_X_FORWARDED_FOR
  | Request::HEADER_X_FORWARDED_HOST
  | Request::HEADER_X_FORWARDED_PORT
  | Request::HEADER_X_FORWARDED_PROTO;

Always validate

Whatever you end up storing, validate it as an IP first. Anything else is asking for SQL injection in your access log table:

if (!filter_var($ip, FILTER_VALIDATE_IP)) {
  $ip = '0.0.0.0';
}

Bonus: don't store IPs longer than you need

Under UK GDPR, an IP address is personal data. If you log them for security purposes, fine — but write a retention policy (90 days is typical) and stick to it. "We've kept access logs since 2014" is a finding waiting to happen.

If your application does anything based on visitor IP — rate limiting, fraud detection, geographic personalisation — and you'd like a second pair of eyes on whether you're doing it safely, we are happy to take a look.

Related notes