Skip to content

Commit 8188f1c

Browse files
MatTheCatfabpot
authored andcommitted
[HttpFoundation] Prevent accepted rate limits with no remaining token to be preferred over denied ones
1 parent b206928 commit 8188f1c

File tree

4 files changed

+117
-4
lines changed

4 files changed

+117
-4
lines changed

src/Symfony/Component/HttpFoundation/RateLimiter/AbstractRequestRateLimiter.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ public function consume(Request $request): RateLimit
3535
foreach ($limiters as $limiter) {
3636
$rateLimit = $limiter->consume(1);
3737

38-
if (null === $minimalRateLimit || $rateLimit->getRemainingTokens() < $minimalRateLimit->getRemainingTokens()) {
39-
$minimalRateLimit = $rateLimit;
40-
}
38+
$minimalRateLimit = $minimalRateLimit ? self::getMinimalRateLimit($minimalRateLimit, $rateLimit) : $rateLimit;
4139
}
4240

4341
return $minimalRateLimit;
@@ -54,4 +52,20 @@ public function reset(Request $request): void
5452
* @return LimiterInterface[] a set of limiters using keys extracted from the request
5553
*/
5654
abstract protected function getLimiters(Request $request): array;
55+
56+
private static function getMinimalRateLimit(RateLimit $first, RateLimit $second): RateLimit
57+
{
58+
if ($first->isAccepted() !== $second->isAccepted()) {
59+
return $first->isAccepted() ? $second : $first;
60+
}
61+
62+
$firstRemainingTokens = $first->getRemainingTokens();
63+
$secondRemainingTokens = $second->getRemainingTokens();
64+
65+
if ($firstRemainingTokens === $secondRemainingTokens) {
66+
return $first->getRetryAfter() < $second->getRetryAfter() ? $second : $first;
67+
}
68+
69+
return $firstRemainingTokens > $secondRemainingTokens ? $second : $first;
70+
}
5771
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Tests\RateLimiter;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\RateLimiter\LimiterInterface;
17+
use Symfony\Component\RateLimiter\RateLimit;
18+
19+
class AbstractRequestRateLimiterTest extends TestCase
20+
{
21+
/**
22+
* @dataProvider provideRateLimits
23+
*/
24+
public function testConsume(array $rateLimits, ?RateLimit $expected)
25+
{
26+
$rateLimiter = new MockAbstractRequestRateLimiter(array_map(function (RateLimit $rateLimit) {
27+
$limiter = $this->createStub(LimiterInterface::class);
28+
$limiter->method('consume')->willReturn($rateLimit);
29+
30+
return $limiter;
31+
}, $rateLimits));
32+
33+
$this->assertSame($expected, $rateLimiter->consume(new Request()));
34+
}
35+
36+
public function provideRateLimits()
37+
{
38+
$now = new \DateTimeImmutable();
39+
40+
yield 'Both accepted with different count of remaining tokens' => [
41+
[
42+
$expected = new RateLimit(0, $now, true, 1), // less remaining tokens
43+
new RateLimit(1, $now, true, 1),
44+
],
45+
$expected,
46+
];
47+
48+
yield 'Both accepted with same count of remaining tokens' => [
49+
[
50+
$expected = new RateLimit(0, $now->add(new \DateInterval('P1D')), true, 1), // longest wait time
51+
new RateLimit(0, $now, true, 1),
52+
],
53+
$expected,
54+
];
55+
56+
yield 'Accepted and denied' => [
57+
[
58+
new RateLimit(0, $now, true, 1),
59+
$expected = new RateLimit(0, $now, false, 1), // denied
60+
],
61+
$expected,
62+
];
63+
}
64+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Tests\RateLimiter;
13+
14+
use Symfony\Component\HttpFoundation\RateLimiter\AbstractRequestRateLimiter;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\RateLimiter\LimiterInterface;
17+
18+
class MockAbstractRequestRateLimiter extends AbstractRequestRateLimiter
19+
{
20+
/**
21+
* @var LimiterInterface[]
22+
*/
23+
private $limiters;
24+
25+
public function __construct(array $limiters)
26+
{
27+
$this->limiters = $limiters;
28+
}
29+
30+
protected function getLimiters(Request $request): array
31+
{
32+
return $this->limiters;
33+
}
34+
}

src/Symfony/Component/HttpFoundation/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"symfony/dependency-injection": "^5.4|^6.0",
2828
"symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4",
2929
"symfony/mime": "^4.4|^5.0|^6.0",
30-
"symfony/expression-language": "^4.4|^5.0|^6.0"
30+
"symfony/expression-language": "^4.4|^5.0|^6.0",
31+
"symfony/rate-limiter": "^5.2|^6.0"
3132
},
3233
"suggest" : {
3334
"symfony/mime": "To use the file extension guesser"

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy