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);
}
}
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: '*');
})
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'),
];
});
Before the fix you'd see something like:
{
"request_ip": "172.70.216.62",
"cf_connecting_ip": "26.55.52.xx"
}
After the fix, request_ip matches cf_connecting_ip the real client IP.
Remove the debug route before going to production :))
Top comments (0)