Skip to content

[RateLimiter][Security] More precisely document advanced rate limiter configuration #15051

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 21, 2021
Merged
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
2 changes: 2 additions & 0 deletions cache.rst
Original file line number Diff line number Diff line change
@@ -183,6 +183,8 @@ will create pools with service IDs that follow the pattern ``cache.[type]``.
],
]);
.. _cache-create-pools:

Creating Custom (Namespaced) Pools
----------------------------------

2 changes: 2 additions & 0 deletions lock.rst
Original file line number Diff line number Diff line change
@@ -228,6 +228,8 @@ processes asking for the same ``$version``::
}
}

.. _lock-named-locks:

Named Lock
----------

243 changes: 208 additions & 35 deletions rate_limiter.rst
Original file line number Diff line number Diff line change
@@ -124,20 +124,74 @@ Configuration
The following example creates two different rate limiters for an API service, to
enforce different levels of service (free or paid):

.. code-block:: yaml
# config/packages/rate_limiter.yaml
framework:
rate_limiter:
anonymous_api:
# use 'sliding_window' if you prefer that policy
policy: 'fixed_window'
limit: 100
interval: '60 minutes'
authenticated_api:
policy: 'token_bucket'
limit: 5000
rate: { interval: '15 minutes', amount: 500 }
.. configuration-block::

.. code-block:: yaml
# config/packages/rate_limiter.yaml
framework:
rate_limiter:
anonymous_api:
# use 'sliding_window' if you prefer that policy
policy: 'fixed_window'
limit: 100
interval: '60 minutes'
authenticated_api:
policy: 'token_bucket'
limit: 5000
rate: { interval: '15 minutes', amount: 500 }
.. code-block:: xml
<!-- config/packages/rate_limiter.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony
https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:rate-limiter>
<!-- policy: use 'sliding_window' if you prefer that policy -->
<framework:limiter name="anonymous_api"
policy="fixed_window"
limit="100"
interval="60 minutes"
/>
<framework:limiter name="authenticated_api"
policy="token_bucket"
limit="5000"
>
<framework:rate interval="15 minutes"
amount="500"
/>
</framework:limiter>
</framework:rate-limiter>
</framework:config>
</container>
.. code-block:: php
// config/packages/rate_limiter.php
$container->loadFromExtension('framework', [
rate_limiter' => [
'anonymous_api' => [
// use 'sliding_window' if you prefer that policy
'policy' => 'fixed_window',
'limit' => 100,
'interval' => '60 minutes',
],
'authenticated_api' => [
'policy' => 'token_bucket',
'limit' => 5000,
'rate' => [ 'interval' => '15 minutes', 'amount' => 500 ],
],
],
]);
.. note::

@@ -300,27 +354,146 @@ the :class:`Symfony\\Component\\RateLimiter\\Reservation` object returned by the
}
}

Rate Limiter Storage and Locking
--------------------------------

Rate limiters use the default cache and locking mechanisms defined in your
Symfony application. If you prefer to change that, use the ``lock_factory`` and
``storage_service`` options:

.. code-block:: yaml
# config/packages/rate_limiter.yaml
framework:
rate_limiter:
anonymous_api_limiter:
# ...
# the value is the name of any cache pool defined in your application
cache_pool: 'app.redis_cache'
# or define a service implementing StorageInterface to use a different
# mechanism to store the limiter information
storage_service: 'App\RateLimiter\CustomRedisStorage'
# the value is the name of any lock defined in your application
lock_factory: 'app.rate_limiter_lock'
Storing Rate Limiter State: Caching
-----------------------------------

All rate limiter policies require to store the state of the rate limiter
(e.g. how many hits were already made in the current time window). This
state is stored by default using the :doc:`Cache component </cache>`.

The default cache pool used by all limiters is ``cache.rate_limiter``. You
can modify this cache pool by :ref:`defining a "rate_limiter" pool <cache-create-pools>`.

You can also override the pool for a specific limiter using the ``cache_pool``
option:

.. configuration-block::

.. code-block:: yaml
# config/packages/rate_limiter.yaml
framework:
rate_limiter:
anonymous_api:
# ...
# use the "cache.anonymous_rate_limiter" cache pool
cache_pool: 'cache.anonymous_rate_limiter'
.. code-block:: xml
<!-- config/packages/rate_limiter.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony
https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:rate-limiter>
<!-- cache-pool: use the "cache.anonymous_rate_limiter" cache pool -->
<framework:limiter name="anonymous_api"
policy="fixed_window"
limit="100"
interval="60 minutes"
cache-pool="cache.anonymous_rate_limiter"
/>
<!-- ... ->
</framework:rate-limiter>
</framework:config>
</container>
.. code-block:: php
// config/packages/rate_limiter.php
$container->loadFromExtension('framework', [
rate_limiter' => [
'anonymous_api' => [
// ...
// use the "cache.anonymous_rate_limiter" cache pool
'cache_pool' => 'cache.anonymous_rate_limiter',
],
],
]);
.. note::
Instead of using the Cache component, you can also implement a custom
storage. Create a PHP class that implements the
:class:`Symfony\\Component\\RateLimiter\\Storage\\StorageInterface` and
set the ``storage_service`` setting of each limiter to the service ID
of this class.
Using Locks to Prevent Race Conditions
--------------------------------------
Rate limiting can be affected by race conditions, if the same limiter is
applied to simultaneous requests (e.g. 3 servers of the same client call
the same API). To prevent these race conditions, the rate limiting
operations are protected using :doc:`locks </lock>`.
By default, the global lock (configured by ``framework.lock``) is used. You
can use a specific :ref:`named lock <lock-named-locks>` via the
``lock_factory`` option:
.. configuration-block::
.. code-block:: yaml
# config/packages/rate_limiter.yaml
framework:
rate_limiter:
anonymous_api:
# ...
# use the "lock.rate_limiter.factory" for this limiter
lock_factory: 'lock.rate_limiter.factory'
.. code-block:: xml
<!-- config/packages/rate_limiter.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony
https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:rate-limiter>
<!-- limiter-factory: use the "lock.rate_limiter.factory" for this limiter -->
<framework:limiter name="anonymous_api"
policy="fixed_window"
limit="100"
interval="60 minutes"
lock-factory="lock.rate_limiter.factory"
/>
<!-- ... -->
</framework:rate-limiter>
</framework:config>
</container>
.. code-block:: php
// config/packages/rate_limiter.php
$container->loadFromExtension('framework', [
rate_limiter' => [
'anonymous_api' => [
// ...
// use the "lock.rate_limiter.factory" for this limiter
'lock_factory' => 'lock.rate_limiter.factory',
],
],
]);
.. _`DoS attacks`: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html
.. _`Apache mod_ratelimit`: https://httpd.apache.org/docs/current/mod/mod_ratelimit.html
144 changes: 135 additions & 9 deletions security.rst
Original file line number Diff line number Diff line change
@@ -550,11 +550,6 @@ You must enable this using the ``login_throttling`` setting:
'login_throttling' => [
'max_attempts' => 3,
],
// use a custom rate limiter via its service ID
'login_throttling' => [
'limiter' => 'app.my_login_rate_limiter',
],
],
],
]);
@@ -565,17 +560,148 @@ failed requests for ``IP address``. The second limit protects against an
attacker using multiple usernames from bypassing the first limit, without
distrupting normal users on big networks (such as offices).

If you need a more complex limiting algorithm, create a class that implements
:class:`Symfony\\Component\\HttpFoundation\\RateLimiter\\RequestRateLimiterInterface`
and set the ``limiter`` option to its service ID.

.. tip::

Limiting the failed login attempts is only one basic protection against
brute force attacks. The `OWASP Brute Force Attacks`_ guidelines mention
several other protections that you should consider depending on the
level of protection required.

If you need a more complex limiting algorithm, create a class that implements
:class:`Symfony\\Component\\HttpFoundation\\RateLimiter\\RequestRateLimiterInterface`
(or use
:class:`Symfony\\Component\\Security\\Http\\RateLimiter\\DefaultLoginRateLimiter`)
and set the ``limiter`` option to its service ID:

.. configuration-block::

.. code-block:: yaml
# config/packages/security.yaml
framework:
rate_limiter:
# define 2 rate limiters (one for username+IP, the other for IP)
username_ip_login:
policy: token_bucket
limit: 5
rate: { interval: '5 minutes' }
ip_login:
policy: sliding_window
limit: 50
interval: '15 minutes'
services:
# our custom login rate limiter
app.login_rate_limiter:
class: Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter
arguments:
# globalFactory is the limiter for IP
$globalFactory: '@limiter.ip_login'
# localFactory is the limiter for username+IP
$localFactory: '@limiter.username_ip_login'
security:
firewalls:
main:
# use a custom rate limiter via its service ID
login_throttling:
limiter: app.login_rate_limiter
.. code-block:: xml
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony
https://symfony.com/schema/dic/symfony/symfony-1.0.xsd
http://symfony.com/schema/dic/security
https://symfony.com/schema/dic/security/security-1.0.xsd">
<framework:config>
<framework:rate-limiter>
<!-- define 2 rate limiters (one for username+IP, the other for IP) -->
<framework:limiter name="username_ip_login"
policy="token_bucket"
limit="5"
>
<framework:rate interval="5 minutes"/>
</framework:limiter>
<framework:limiter name="ip_login"
policy="sliding_window"
limit="50"
interval="15 minutes"
/>
</framework:rate-limiter>
</framework:config>
<srv:services>
<!-- our custom login rate limiter -->
<srv:service id="app.login_rate_limiter"
class="Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter"
>
<!-- 1st argument is the limiter for IP -->
<srv:argument type="service" id="limiter.ip_login"/>
<1-- 2nd argument is the limiter for username+IP -->
<srv:argument type="service" id="limiter.username_ip_login"/>
</srv:service>
</srv:services>
<config>
<firewall name="main">
<!-- use a custom rate limiter via its service ID -->
<login-throttling limiter="app.login_rate_limiter"/>
</firewall>
</config>
</srv:container>
.. code-block:: php
// config/packages/security.php
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter;
$container->loadFromExtension('framework', [
'rate_limiter' => [
// define 2 rate limiters (one for username+IP, the other for IP)
'username_ip_login' => [
'policy' => 'token_bucket',
'limit' => 5,
'rate' => [ 'interval' => '5 minutes' ],
],
'ip_login' => [
'policy' => 'sliding_window',
'limit' => 50,
'interval' => '15 minutes',
],
],
]);
$container->register('app.login_rate_limiter', DefaultLoginRateLimiter::class)
->setArguments([
// 1st argument is the limiter for IP
new Reference('limiter.ip_login'),
// 2nd argument is the limiter for username+IP
new Reference('limiter.username_ip_login'),
]);
$container->loadFromExtension('security', [
'firewalls' => [
'main' => [
// use a custom rate limiter via its service ID
'login_throttling' =>
'limiter' => 'app.login_rate_limiter',
],
],
],
]);
.. _`security-authorization`:
.. _denying-access-roles-and-other-authorization: