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

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,16 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
];
$this->registerRateLimiter($container, $localId = '_login_local_'.$firewallName, $limiterOptions);

$this->registerRateLimiter($container, $usernameId = '_login_username_'.$firewallName, $limiterOptions);

$limiterOptions['limit'] = 5 * $config['max_attempts'];
$this->registerRateLimiter($container, $globalId = '_login_global_'.$firewallName, $limiterOptions);

$container->register($config['limiter'] = 'security.login_throttling.'.$firewallName.'.limiter', DefaultLoginRateLimiter::class)
->addArgument(new Reference('limiter.'.$globalId))
->addArgument(new Reference('limiter.'.$localId))
->addArgument(new Parameter('container.build_hash'))
->addArgument(new Reference('limiter.'.$usernameId))
;
}

Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Security/Http/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ CHANGELOG
* Add `this` to `#[IsGranted]` subject expression variables when available
* Add support for closures and `this` in `#[IsCsrfTokenValid]` when evaluating its `id`
* Deprecate the `$eraseCredentials` argument of `AuthenticatorManager::__construct()`, as the `eraseCredentials()` method was removed in Symfony 8.0
* Add a per-username rate limit to `DefaultLoginRateLimiter` to prevent brute-force attacks from multiple IPs targeting a single account

8.0
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
/**
* A default login throttling limiter.
*
* This limiter prevents breadth-first attacks by enforcing
* a limit on username+IP and a (higher) limit on IP.
* This limiter prevents breadth-first and distributed brute-force attacks by
* enforcing three limits in sequence:
* 1. IP only (global): blocks wide scans from a single IP;
* 2. username + IP (local): blocks targeted attacks from a single IP;
* 3. username only: blocks distributed botnet attacks across many IPs.
*
* @author Wouter de Jong <[email protected]>
*/
Expand All @@ -34,6 +37,7 @@ public function __construct(
private RateLimiterFactory $globalFactory,
private RateLimiterFactory $localFactory,
#[\SensitiveParameter] private string $secret,
private ?RateLimiterFactory $usernameFactory = null,
) {
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
Expand All @@ -45,10 +49,16 @@ protected function getLimiters(Request $request): array
$username = $request->attributes->get(SecurityRequestAttributes::LAST_USERNAME, '');
$username = preg_match('//u', $username) ? mb_strtolower($username, 'UTF-8') : strtolower($username);

return [
$limiters = [
$this->globalFactory->create($this->hash($request->getClientIp())),
$this->localFactory->create($this->hash($username.'-'.$request->getClientIp())),
];

if (null !== $this->usernameFactory) {
$limiters[] = $this->usernameFactory->create($this->hash($username));
}

return $limiters;
}

private function hash(string $data): string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,25 @@ protected function setUp(): void
{
$this->requestStack = new RequestStack();

$globalLimiter = new RateLimiterFactory([
'id' => 'login',
'policy' => 'fixed_window',
'limit' => 6,
'interval' => '1 minute',
], new InMemoryStorage());
$localLimiter = new RateLimiterFactory([
'id' => 'login',
'policy' => 'fixed_window',
'limit' => 3,
'interval' => '1 minute',
], new InMemoryStorage());
$globalLimiter = new RateLimiterFactory([
$usernameLimiter = new RateLimiterFactory([
'id' => 'login',
'policy' => 'fixed_window',
'limit' => 6,
'limit' => 3,
'interval' => '1 minute',
], new InMemoryStorage());
$limiter = new DefaultLoginRateLimiter($globalLimiter, $localLimiter, '$3cre7');
$limiter = new DefaultLoginRateLimiter($globalLimiter, $localLimiter, '$3cre7', $usernameLimiter);

$this->listener = new LoginThrottlingListener($this->requestStack, $limiter);
}
Expand Down Expand Up @@ -84,6 +90,25 @@ public function testPreventsLoginWithMultipleCase()
$this->listener->checkPassport($this->createCheckPassportEvent($passports[0]));
}

public function testPreventsLoginWhenOverUsernameThreshold()
{
$passport = $this->createPassport('wouter');
// Simulate requests from different IPs
for ($i = 0; $i < 3; ++$i) {
$request = $this->createRequest('10.0.0.'.$i);
$this->requestStack->push($request);
$this->listener->checkPassport($this->createCheckPassportEvent($passport));
$this->listener->onFailedLogin($this->createLoginFailedEvent($passport));
$this->requestStack->pop();
}

// A new IP should still be blocked because the username limit is reached
$request = $this->createRequest('10.0.1.0');
$this->requestStack->push($request);
$this->expectException(TooManyLoginAttemptsAuthenticationException::class);
$this->listener->checkPassport($this->createCheckPassportEvent($passport));
}

public function testPreventsLoginWhenOverGlobalThreshold()
{
$request = $this->createRequest();
Expand Down
Loading