commit 60a1b33dfa3065e5a8c9d2f9df428860fb902408 Author: peterh peterh@giantrabbit.com Date: Wed Jan 22 16:03:01 2020 -0800
Rate limit number of subscription requests
An attacker could use the /subscribe form to send tons of emails to anyone's email address. We want to limit that so it doesn't cause problem. This limits it to 10 emails per 6 hours. It's actually doing it by rate, so once you hit the limit of 10, then you can send another one about 36 minutes after that and keep sending one every 36 minutes.
Issue #44700 --- src/IpRateExceeded.php | 6 +++++ src/IpRateLimiter.php | 57 ++++++++++++++++++++++++++++++++++++++++++ src/SubscriptionController.php | 2 ++ src/dependencies.php | 4 +++ src/middleware.php | 1 + src/settings.php | 4 +++ 6 files changed, 74 insertions(+)
diff --git a/src/IpRateExceeded.php b/src/IpRateExceeded.php new file mode 100644 index 00000000..8b778993 --- /dev/null +++ b/src/IpRateExceeded.php @@ -0,0 +1,6 @@ +<?php + +namespace Tor; + +class IpRateExceeded extends \Exception { +} diff --git a/src/IpRateLimiter.php b/src/IpRateLimiter.php new file mode 100644 index 00000000..b14af3d3 --- /dev/null +++ b/src/IpRateLimiter.php @@ -0,0 +1,57 @@ +<?php + +namespace Tor; + +class IpRateLimiter { + public $redis; + public $environment_info; + + public function __construct($container) { + $this->environment_info = $container->get('environment_info'); + $this->logger = $container->get('logger'); + $this->redis = \Resque::redis(); + $this->settings = $container->get('settings')['ipRateLimiter']; + $this->maxRequestsPerTimeSpan = $this->settings['maxRequestsPerTimeSpan']; + $this->timeSpan = $this->settings['timeSpan']; + } + + function check($request) { + $keyName = $this->keyName($request); + list($allowance, $lastCheck) = $this->getIpData($keyName); + $now = time(); + $timePassed = $now - $lastCheck; + $allowanceAdjustment = $timePassed * $this->maxRequestsPerTimeSpan / $this->timeSpan; + $allowance += $allowanceAdjustment; + if ($allowance < 1) { + $this->setIpData($keyName, $allowance, $now); + $ipAddress = $request->getAttribute('ip_address'); + throw new IpRateExceeded("There have been more than {$this->maxRequestsPerTimeSpan} requests from $ipAddress in the last {$this->timeSpan} seconds."); + } + $allowance -= 1; + $this->setIpData($keyName, $allowance, $now); + } + + function getIpData($keyName) { + $data = $this->redis->get($keyName); + if (is_null($data)) { + return [$this->maxRequestsPerTimeSpan, time()]; + } + $struct = unserialize($data, ['allowed_classes', FALSE]); + if ($struct === FALSE) { + $this->logger->debug("Bap\n!"); + return [$this->maxRequestsPerTimeSpan, time()]; + } + return unserialize($data); + } + + function keyName($request) { + $ipAddress = $request->getAttribute('ip_address'); + return $this->environment_info->name() . "_rate_limiter_$ipAddress"; + } + + function setIpData($keyName, $allowance, $lastCheck) { + $data = serialize([$allowance, $lastCheck]); + $this->redis->set($keyName, $data); + $this->redis->expire($keyName, $this->timeSpan); + } +} diff --git a/src/SubscriptionController.php b/src/SubscriptionController.php index d403d77e..6aa60707 100644 --- a/src/SubscriptionController.php +++ b/src/SubscriptionController.php @@ -60,6 +60,8 @@ class SubscriptionController extends BaseController { }
public function subscriptionRequest($request, $response, $args) { + $ipRateLimiter = $this->container->get('ipRateLimiter'); + $ipRateLimiter->check($request); $this->vars['bodyClasses'] = array('subscribe'); $parsedBody = $request->getParsedBody(); $fieldValidationRules = array( diff --git a/src/dependencies.php b/src/dependencies.php index 68cd24d0..65058873 100644 --- a/src/dependencies.php +++ b/src/dependencies.php @@ -67,6 +67,10 @@ $container['errorHandler'] = function($container) { return $error_handler; };
+$container['ipRateLimiter'] = function($container) { + return new \Tor\IpRateLimiter($container); +}; + $settings = $container->get('settings'); $settings['redis'] = [ 'host' => 'localhost', diff --git a/src/middleware.php b/src/middleware.php index d0f7da59..a2c120df 100644 --- a/src/middleware.php +++ b/src/middleware.php @@ -3,3 +3,4 @@
// e.g: $app->add(new \Slim\Csrf\Guard); $app->add(new \Tor\I18nMiddleware($app)); +$app->add(new RKA\Middleware\IpAddress()); diff --git a/src/settings.php b/src/settings.php index 04934131..c615f9c1 100644 --- a/src/settings.php +++ b/src/settings.php @@ -6,6 +6,10 @@ $config = [ 'settings' => [ 'displayErrorDetails' => true, 'addContentLengthHeader' => false, + 'ipRateLimiter' => [ + 'maxRequestsPerTimeSpan' => 10, + 'timeSpan' => 21600, + ], 'renderer' => [ 'cache_path' => __DIR__ . '/../tmp/cache/templates', 'template_path' => __DIR__ . '/../templates/',
tor-commits@lists.torproject.org