DEV Community

Cover image for Getting Real Client IPs Behind Cloudflare Proxy in Laravel
Ozan
Ozan

Posted on

Getting Real Client IPs Behind Cloudflare Proxy in Laravel

If your server sits behind Cloudflare with a firewall that only allows
Cloudflare IP ranges, $request->ip() returns the Cloudflare proxy IP,
not the real client IP.

Why X-Forwarded-For Isn't Enough

The common advice is to use trustProxies(at: '*') and read
X-Forwarded-For. The problem: users can inject fake entries into this
header before it reaches Cloudflare, and Cloudflare appends rather than
replaces:

User sends:

X-Forwarded-For: 1.1.1.1

Cloudflare appends its own, resulting in:

X-Forwarded-For: 1.1.1.1, 26.55.52.xx, 172.70.216.62

With trustProxies(at: '*'), Laravel trusts everything and returns the
leftmost IP — the spoofed one.

CF-Connecting-IP Is the Right Header

Cloudflare sets CF-Connecting-IP to the actual connecting client IP and
strips any user-supplied header with the same name. It cannot be spoofed.

The Fix: A Simple Middleware

// app/Http/Middleware/CloudflareRealIp.php

  namespace App\Http\Middleware;

  use Closure;
  use Illuminate\Http\Request;
  use Symfony\Component\HttpFoundation\Response;

  class CloudflareRealIp
  {
      public function handle(Request $request, Closure $next): Response
      {
          $realIp = $request->header('CF-Connecting-IP');

            if ($realIp && filter_var($realIp, FILTER_VALIDATE_IP)) {
              $request->server->set('REMOTE_ADDR', $realIp);
              $request->headers->set('X-Forwarded-For', $realIp);
            }

          return $next($request);
      }
  }
Enter fullscreen mode Exit fullscreen mode

Register it in bootstrap/app.php — prepend ensures it runs before
Laravel's TrustProxies middleware:

->withMiddleware(function (Middleware $middleware): void {
      $middleware->prepend(CloudflareRealIp::class);
      $middleware->trustProxies(at: '*');
})
Enter fullscreen mode Exit fullscreen mode

Why Not Check Cloudflare IP Ranges?

You might want to verify that the request actually comes from Cloudflare
before trusting CF-Connecting-IP. However, if your firewall already
blocks all non-Cloudflare traffic at the network level, this check is
redundant — and it breaks when a load balancer or internal proxy sits
between Cloudflare and your app (REMOTE_ADDR becomes an internal IP like
10.0.1.6 instead of a Cloudflare IP).

If you don't have a firewall, add the range check — but if you do, skip it.

Verify It Works

Add a quick debug route:

Route::get('/ip-check', function (Request $request) {
      return [
          'request_ip'      => $request->ip(),
          'remote_addr'     => $request->server('REMOTE_ADDR'),
          'x_forwarded_for' => $request->header('X-Forwarded-For'),
          'cf_connecting_ip'=> $request->header('CF-Connecting-IP'),
      ];
  });
Enter fullscreen mode Exit fullscreen mode

Before the fix you'd see something like:

  {
    "request_ip": "172.70.216.62",
    "cf_connecting_ip": "26.55.52.xx"
  }
Enter fullscreen mode Exit fullscreen mode

After the fix, request_ip matches cf_connecting_ip the real client IP.

Remove the debug route before going to production :))

Top comments (0)