fix(auth): use Cloudflare client IP for authentication rate limits
This commit is contained in:
parent
9412566d68
commit
bf20c8b287
7 changed files with 84 additions and 4 deletions
|
|
@ -92,7 +92,7 @@ private function ensureRegistrationIsNotRateLimited(array $input): void
|
|||
{
|
||||
$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,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ protected function configureRateLimiting(): void
|
|||
});
|
||||
|
||||
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 @@ protected function configureRateLimiting(): void
|
|||
|
||||
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 @@ protected function configureRateLimiting(): void
|
|||
});
|
||||
|
||||
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
bootstrap/helpers/auth.php
Normal file
14
bootstrap/helpers/auth.php
Normal 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 @@
|
|||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config()->set('app.maintenance.store', 'array');
|
||||
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
RateLimiter::clear('login');
|
||||
|
||||
|
|
@ -66,3 +68,27 @@
|
|||
$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();
|
||||
});
|
||||
|
|
|
|||
36
tests/Feature/AuthRateLimitClientIpTest.php
Normal file
36
tests/Feature/AuthRateLimitClientIpTest.php
Normal file
|
|
@ -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 @@
|
|||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config()->set('app.maintenance.store', 'array');
|
||||
|
||||
InstanceSettings::query()->forceCreate(['id' => 0]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config()->set('app.maintenance.store', 'array');
|
||||
|
||||
InstanceSettings::query()->forceCreate([
|
||||
'id' => 0,
|
||||
'is_registration_enabled' => true,
|
||||
|
|
|
|||
Loading…
Reference in a new issue