fix(auth): use Cloudflare client IP for authentication rate limits

This commit is contained in:
Andras Bacsai
2026-08-26 17:25:28 +02:00
parent 9412566d68
commit bf20c8b287
7 changed files with 84 additions and 4 deletions
+1 -1
View File
@@ -92,7 +92,7 @@ class CreateNewUser implements CreatesNewUsers
{
$keys = [
[
'key' => 'registration:ip:'.sha1((string) request()->ip()),
'key' => 'registration:ip:'.sha1(auth_rate_limit_ip(request())),
'max' => self::REGISTRATION_IP_MAX_ATTEMPTS,
'decay' => self::REGISTRATION_IP_DECAY_SECONDS,
],
+3 -3
View File
@@ -60,7 +60,7 @@ class RouteServiceProvider extends ServiceProvider
});
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by((string) $request->email.'|'.$request->ip());
return Limit::perMinute(5)->by((string) $request->email.'|'.auth_rate_limit_ip($request));
});
RateLimiter::for('two-factor', function (Request $request) {
@@ -69,7 +69,7 @@ class RouteServiceProvider extends ServiceProvider
RateLimiter::for('forgot-password', function (Request $request) {
$limits = [
Limit::perMinutes(10, 3)->by('forgot-password:ip:'.sha1((string) $request->ip())),
Limit::perMinutes(10, 3)->by('forgot-password:ip:'.sha1(auth_rate_limit_ip($request))),
];
$emailIdentity = normalize_email_identity($request->input('email'));
@@ -81,7 +81,7 @@ class RouteServiceProvider extends ServiceProvider
});
RateLimiter::for('magic-link', function (Request $request) {
return Limit::perMinute(5)->by(hash('sha256', (string) $request->input('token').'|'.$request->ip()));
return Limit::perMinute(5)->by(hash('sha256', (string) $request->input('token').'|'.auth_rate_limit_ip($request)));
});
RateLimiter::for('force-password-reset', function (Request $request) {
+14
View File
@@ -0,0 +1,14 @@
<?php
use Illuminate\Http\Request;
function auth_rate_limit_ip(Request $request): string
{
$cloudflareIp = $request->header('CF-Connecting-IP');
if (isCloud() && is_string($cloudflareIp) && filter_var($cloudflareIp, FILTER_VALIDATE_IP) !== false) {
return $cloudflareIp;
}
return (string) $request->ip();
}
@@ -8,6 +8,8 @@ use Illuminate\Support\Facades\RateLimiter;
uses(RefreshDatabase::class);
beforeEach(function () {
config()->set('app.maintenance.store', 'array');
InstanceSettings::forceCreate(['id' => 0]);
RateLimiter::clear('login');
@@ -66,3 +68,27 @@ test('successful login is still possible within rate limit', function () {
$response->assertRedirect();
expect($response->status())->not->toBe(429);
});
test('cloud login rate limits use the Cloudflare client ip', function () {
config()->set('constants.coolify.self_hosted', false);
foreach (range(1, 5) as $attempt) {
$this->withHeader('CF-Connecting-IP', '2001:db8::10')
->withHeader('X-Forwarded-For', '2001:db8::10, 108.162.221.29')
->withServerVariables(['REMOTE_ADDR' => '10.0.0.5'])
->post('/login', [
'email' => 'test@example.com',
'password' => 'wrong-password',
])
->assertRedirect();
}
$this->withHeader('CF-Connecting-IP', '2001:db8::20')
->withHeader('X-Forwarded-For', '2001:db8::20, 108.162.221.29')
->withServerVariables(['REMOTE_ADDR' => '10.0.0.5'])
->post('/login', [
'email' => 'test@example.com',
'password' => 'wrong-password',
])
->assertRedirect();
});
@@ -0,0 +1,36 @@
<?php
use Illuminate\Http\Request;
it('uses the Cloudflare client ip on cloud', function () {
config()->set('constants.coolify.self_hosted', false);
$request = Request::create('/', server: [
'REMOTE_ADDR' => '10.0.0.5',
'HTTP_CF_CONNECTING_IP' => '2001:db8::10',
]);
expect(auth_rate_limit_ip($request))->toBe('2001:db8::10');
});
it('falls back to the resolved request ip when the Cloudflare header is invalid', function () {
config()->set('constants.coolify.self_hosted', false);
$request = Request::create('/', server: [
'REMOTE_ADDR' => '203.0.113.10',
'HTTP_CF_CONNECTING_IP' => 'invalid',
]);
expect(auth_rate_limit_ip($request))->toBe('203.0.113.10');
});
it('ignores the Cloudflare header on self-hosted instances', function () {
config()->set('constants.coolify.self_hosted', true);
$request = Request::create('/', server: [
'REMOTE_ADDR' => '203.0.113.20',
'HTTP_CF_CONNECTING_IP' => '2001:db8::20',
]);
expect(auth_rate_limit_ip($request))->toBe('203.0.113.20');
});
@@ -6,6 +6,8 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
config()->set('app.maintenance.store', 'array');
InstanceSettings::query()->forceCreate(['id' => 0]);
});
@@ -8,6 +8,8 @@ use Illuminate\Support\Facades\RateLimiter;
uses(RefreshDatabase::class);
beforeEach(function () {
config()->set('app.maintenance.store', 'array');
InstanceSettings::query()->forceCreate([
'id' => 0,
'is_registration_enabled' => true,