From 4933d453413a9c523abec68fd8a0df04c8f25d11 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Tue, 2 Mar 2021 15:04:43 +0100 Subject: [PATCH] [RateLimiter][Security] More precisely document advanced rate limiter configuration --- cache.rst | 2 + lock.rst | 2 + rate_limiter.rst | 243 ++++++++++++++++++++++++++++++++++++++++------- security.rst | 144 ++++++++++++++++++++++++++-- 4 files changed, 347 insertions(+), 44 deletions(-) diff --git a/cache.rst b/cache.rst index a5e20950c03..ee32fcc2db2 100644 --- a/cache.rst +++ b/cache.rst @@ -183,6 +183,8 @@ will create pools with service IDs that follow the pattern ``cache.[type]``. ], ]); +.. _cache-create-pools: + Creating Custom (Namespaced) Pools ---------------------------------- diff --git a/lock.rst b/lock.rst index 6a62c558fd7..db987f1d20e 100644 --- a/lock.rst +++ b/lock.rst @@ -228,6 +228,8 @@ processes asking for the same ``$version``:: } } +.. _lock-named-locks: + Named Lock ---------- diff --git a/rate_limiter.rst b/rate_limiter.rst index 99617f20d59..1f900547ea7 100644 --- a/rate_limiter.rst +++ b/rate_limiter.rst @@ -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 + + + + + + + + + + + + + + + + + + .. 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 `. + +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 `. + +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 + + + + + + + + + + + + + + + + + + + + + + + + + .. 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 diff --git a/security.rst b/security.rst index 3b9341a07d6..ea7896e8040 100644 --- a/security.rst +++ b/security.rst @@ -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,10 +560,6 @@ 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 @@ -576,6 +567,141 @@ and set the ``limiter`` option to its service ID. 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 + + + + + + + + + + + + + + + + + + + + + + <1-- 2nd argument is the limiter for username+IP --> + + + + + + + + + + + + + .. 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: