[tor-commits] [donate/master] Rate limit number of subscription requests

peterh at torproject.org peterh at torproject.org
Wed Jun 10 22:48:37 UTC 2020


commit 60a1b33dfa3065e5a8c9d2f9df428860fb902408
Author: peterh <peterh at 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/',





More information about the tor-commits mailing list