Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b4a3196

Browse files
[Security] Add per-username login rate-limit to prevent brute-force attacks
1 parent d628620 commit b4a3196

4 files changed

Lines changed: 39 additions & 5 deletions

File tree

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,15 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
7676
];
7777
$this->registerRateLimiter($container, $localId = '_login_local_'.$firewallName, $limiterOptions);
7878

79+
$this->registerRateLimiter($container, $usernameId = '_login_username_'.$firewallName, $limiterOptions);
80+
7981
$limiterOptions['limit'] = 5 * $config['max_attempts'];
8082
$this->registerRateLimiter($container, $globalId = '_login_global_'.$firewallName, $limiterOptions);
8183

8284
$container->register($config['limiter'] = 'security.login_throttling.'.$firewallName.'.limiter', DefaultLoginRateLimiter::class)
8385
->addArgument(new Reference('limiter.'.$globalId))
8486
->addArgument(new Reference('limiter.'.$localId))
87+
->addArgument(new Reference('limiter.'.$usernameId))
8588
->addArgument(new Parameter('container.build_hash'))
8689
;
8790
}

src/Symfony/Component/Security/Http/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Add `this` to `#[IsGranted]` subject expression variables when available
99
* Add support for closures and `this` in `#[IsCsrfTokenValid]` when evaluating its `id`
1010
* Deprecate the `$eraseCredentials` argument of `AuthenticatorManager::__construct()`, as the `eraseCredentials()` method was removed in Symfony 8.0
11+
* Add a per-username rate limit to `DefaultLoginRateLimiter` to prevent brute-force attacks from multiple IPs targeting a single account
1112

1213
8.0
1314
---

src/Symfony/Component/Security/Http/RateLimiter/DefaultLoginRateLimiter.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@
2020
/**
2121
* A default login throttling limiter.
2222
*
23-
* This limiter prevents breadth-first attacks by enforcing
24-
* a limit on username+IP and a (higher) limit on IP.
23+
* This limiter prevents breadth-first and distributed brute-force attacks by
24+
* enforcing three limits in sequence:
25+
* 1. IP only (global): blocks wide scans from a single IP;
26+
* 2. username + IP (local): blocks targeted attacks from a single IP;
27+
* 3. username only: blocks distributed botnet attacks across many IPs.
2528
*
2629
* @author Wouter de Jong <[email protected]>
2730
*/
@@ -33,6 +36,7 @@ final class DefaultLoginRateLimiter extends AbstractRequestRateLimiter
3336
public function __construct(
3437
private RateLimiterFactory $globalFactory,
3538
private RateLimiterFactory $localFactory,
39+
private RateLimiterFactory $usernameFactory,
3640
#[\SensitiveParameter] private string $secret,
3741
) {
3842
if (!$secret) {
@@ -48,6 +52,7 @@ protected function getLimiters(Request $request): array
4852
return [
4953
$this->globalFactory->create($this->hash($request->getClientIp())),
5054
$this->localFactory->create($this->hash($username.'-'.$request->getClientIp())),
55+
$this->usernameFactory->create($this->hash($username)),
5156
];
5257
}
5358

src/Symfony/Component/Security/Http/Tests/EventListener/LoginThrottlingListenerTest.php

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,25 @@ protected function setUp(): void
3535
{
3636
$this->requestStack = new RequestStack();
3737

38+
$globalLimiter = new RateLimiterFactory([
39+
'id' => 'login',
40+
'policy' => 'fixed_window',
41+
'limit' => 6,
42+
'interval' => '1 minute',
43+
], new InMemoryStorage());
3844
$localLimiter = new RateLimiterFactory([
3945
'id' => 'login',
4046
'policy' => 'fixed_window',
4147
'limit' => 3,
4248
'interval' => '1 minute',
4349
], new InMemoryStorage());
44-
$globalLimiter = new RateLimiterFactory([
50+
$usernameLimiter = new RateLimiterFactory([
4551
'id' => 'login',
4652
'policy' => 'fixed_window',
47-
'limit' => 6,
53+
'limit' => 3,
4854
'interval' => '1 minute',
4955
], new InMemoryStorage());
50-
$limiter = new DefaultLoginRateLimiter($globalLimiter, $localLimiter, '$3cre7');
56+
$limiter = new DefaultLoginRateLimiter($globalLimiter, $localLimiter, $usernameLimiter, '$3cre7');
5157

5258
$this->listener = new LoginThrottlingListener($this->requestStack, $limiter);
5359
}
@@ -84,6 +90,25 @@ public function testPreventsLoginWithMultipleCase()
8490
$this->listener->checkPassport($this->createCheckPassportEvent($passports[0]));
8591
}
8692

93+
public function testPreventsLoginWhenOverUsernameThreshold()
94+
{
95+
$passport = $this->createPassport('wouter');
96+
// Simulate requests from different IPs
97+
for ($i = 0; $i < 3; ++$i) {
98+
$request = $this->createRequest('10.0.0.'.$i);
99+
$this->requestStack->push($request);
100+
$this->listener->checkPassport($this->createCheckPassportEvent($passport));
101+
$this->listener->onFailedLogin($this->createLoginFailedEvent($passport));
102+
$this->requestStack->pop();
103+
}
104+
105+
// A new IP should still be blocked because the username limit is reached
106+
$request = $this->createRequest('10.0.1.0');
107+
$this->requestStack->push($request);
108+
$this->expectException(TooManyLoginAttemptsAuthenticationException::class);
109+
$this->listener->checkPassport($this->createCheckPassportEvent($passport));
110+
}
111+
87112
public function testPreventsLoginWhenOverGlobalThreshold()
88113
{
89114
$request = $this->createRequest();

0 commit comments

Comments
 (0)