changeset 6048:ee7209311a0e

surge protection for authentication (currently just for "moin" auth), updated docs/CHANGES also: improve docstrings, clean up
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Fri, 06 Jun 2014 15:52:35 +0200
parents a9bfc8e99775
children a9567770da68
files MoinMoin/auth/__init__.py MoinMoin/config/multiconfig.py MoinMoin/web/utils.py docs/CHANGES
diffstat 4 files changed, 68 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/auth/__init__.py	Fri Jun 06 14:23:58 2014 +0200
+++ b/MoinMoin/auth/__init__.py	Fri Jun 06 15:52:35 2014 +0200
@@ -140,6 +140,7 @@
 from werkzeug import redirect, abort, url_quote, url_quote_plus
 
 from MoinMoin import user, wikiutil
+from MoinMoin.web.utils import check_surge_protect
 from MoinMoin.util.abuse import log_attempt
 
 
@@ -243,6 +244,9 @@
         if username and not password:
             return ContinueLogin(user_obj, _('Missing password. Please enter user name and password.'))
 
+        check_surge_protect(request, action='auth-ip')
+        check_surge_protect(request, action='auth-name', username=username)
+
         u = user.User(request, name=username, password=password, auth_method=self.name)
         if u.valid:
             logging.debug("%s: successfully authenticated user %r (valid)" % (self.name, u.name))
--- a/MoinMoin/config/multiconfig.py	Fri Jun 06 14:23:58 2014 +0200
+++ b/MoinMoin/config/multiconfig.py	Fri Jun 06 15:52:35 2014 +0200
@@ -858,6 +858,9 @@
         # (like photo galleries) triggering surge protection, we assign rather high limits:
         'AttachFile': (300, 30),
         'cache': (600, 30), # cache action is very cheap/efficient
+        # special stuff to prevent someone trying lots of usernames / passwords to log in:
+        'auth-ip': (10, 3600),  # same remote ip (any name)
+        'auth-name': (10, 3600),  # same name (any remote ip)
      },
      "Surge protection tries to deny clients causing too much load/traffic, see HelpOnConfiguration/SurgeProtection."),
     ('surge_lockout_time', 3600, "time [s] someone gets locked out when ignoring the warnings"),
--- a/MoinMoin/web/utils.py	Fri Jun 06 14:23:58 2014 +0200
+++ b/MoinMoin/web/utils.py	Fri Jun 06 15:52:35 2014 +0200
@@ -42,12 +42,15 @@
                 raise Forbidden()
     return False
 
-def check_surge_protect(request, kick=False):
+def check_surge_protect(request, kick=False, action=None, username=None):
     """ Check for excessive requests
 
     Raises a SurgeProtection exception on wiki overuse.
 
     @param request: a moin request object
+    @param kick: immediately ban this user
+    @param action: specify the action explicitly (default: request.action)
+    @param username: give username (for action == 'auth-name')
     """
     limits = request.cfg.surge_action_limits
     if not limits:
@@ -58,8 +61,26 @@
         return False
 
     validuser = request.user.valid
-    current_id = validuser and request.user.name or remote_addr
-    current_action = request.action
+    current_action = action or request.action
+    if current_action == 'auth-ip':
+        # for checking if some specific ip tries to authenticate too often,
+        # not considering the username it tries to authenticate as (could
+        # be many different names)
+        if current_action not in limits:
+            # if admin did not add this key to the limits configuration, do nothing
+            return False
+        current_id = remote_addr
+    elif current_action == 'auth-name':
+        # for checking if some username tries to authenticate too often,
+        # not considering the ip the request comes from (could be a distributed
+        # attack on a high-privilege user)
+        if current_action not in limits:
+            # if admin did not add this key to the limits configuration, do nothing
+            return False
+        current_id = username
+    else:
+        # general case
+        current_id = validuser and request.user.name or remote_addr
 
     default_limit = limits.get('default', (30, 60))
 
@@ -97,10 +118,10 @@
                 timestamps.append((now + request.cfg.surge_lockout_time, surge_indicator)) # continue like that and get locked out
 
         if current_action not in ('cache', 'AttachFile', ): # don't add cache/AttachFile accesses to all or picture galleries will trigger SP
-            current_action = 'all' # put a total limit on user's requests
-            maxnum, dt = limits.get(current_action, default_limit)
+            action = 'all' # put a total limit on user's requests
+            maxnum, dt = limits.get(action, default_limit)
             events = surgedict.setdefault(current_id, {})
-            timestamps = events.setdefault(current_action, [])
+            timestamps = events.setdefault(action, [])
 
             if kick: # ban this guy, NOW
                 timestamps.extend([(now + request.cfg.surge_lockout_time, "!")] * (2 * maxnum))
@@ -127,6 +148,7 @@
         logging.info("Trusted user %s would have triggered surge protection if not trusted.", request.user.name)
         return False
     elif surge_detected:
+        logging.warning("Surge Protection: action=%s id=%s (ip: %s)", current_action, current_id, remote_addr)
         raise SurgeProtection(retry_after=request.cfg.surge_lockout_time)
     else:
         return False
--- a/docs/CHANGES	Fri Jun 06 14:23:58 2014 +0200
+++ b/docs/CHANGES	Fri Jun 06 15:52:35 2014 +0200
@@ -51,6 +51,39 @@
       * "moin" auth
       * "given" auth
       * setuid (when superuser switches to another user)
+  * surge protection for authentication (currently only for MoinAuth):
+    a) surge protect by IP
+       This covers the case someone is trying to authenticate way too
+       often - we don't look at the username here, just at the remote IP
+       address. If surge protection kicks in for some specific IP, that IP
+       won't be able to try to authenticate any more until surge_lockout_time
+       is over.
+       Note: be careful with users behind proxies or NAT routers - these are
+             common and legitimate cases with (potentially lots of)
+             authentication requests coming from same IP.
+             if it is a trusted proxy, you can configure moin so it sees the
+             real remote IP address (not just the proxy's address).
+    b) surge protect by name
+       This covers the case someone is trying to authenticate for a
+       specific user name way too often (e.g. when someone tries to attack the
+       wiki admin's account). We don't look at the IP here, just at the user
+       name. If surge protection kicks in for some specific user name, that user
+       name will not be able to try to authenticate any more until
+       surge_lockout_time is over.
+       Note: this even covers widely distributed attacks against a user, but
+             you should only enable this if you are aware that the "real" user
+             also won't be able to authenticate while surge protection is active
+             (at least not using the account for that specific username).
+             Thus, there is some denial-of-service danger with this if the
+             attacker can guess or find your valid user names (which isn't too
+             difficult if your wiki is publicly readable).
+             This is bad, but technically hard to avoid.
+    Configuration (allowing 10 authentication attempts per hour):
+        surge_action_limits = {
+            # ...
+            'auth-ip': (10, 3600),  # same remote ip (any name)
+            'auth-name': (10, 3600),  # same name (any remote ip)
+         }
   * backlinks performance tuning: the pagename in the theme has historically
     been used to trigger a "linkto:ThisPage" search. While this is a nice
     feature for human users of the wiki (esp. on category pages), it has one