Mercurial > p > roundup > code
diff roundup/cgi/actions.py @ 5717:cad18de2b988
issue2550949: Rate limit password guesses/login attempts.
Generic rate limit mechanism added. Deployed for web page
logins. Default is 3 login attempts/minute for a user. After which one
login attempt every 20 seconds can be done.
Uses gcra algorithm so all I need to store is a username and timestamp
in the one time key database. This does mean I don't have a list of
all failed login attempts as part of the rate limiter.
Set up config setting as well so admin can tune the rate. Maybe 1
every 10 seconds is ok at a site with poor typists who need 6 attempts
to get the password right 8-).
The gcra method can also be used to limit the rest and xmlrpc
interfaces if needed. The mechanism I added also supplies a status
method that calculates the expected values for http headers returned
as part of rate limiting.
Also tests added to test all code paths I hope.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Sat, 11 May 2019 17:24:58 -0400 |
| parents | 9689d1bf9bb0 |
| children | 842252c3ee22 |
line wrap: on
line diff
--- a/roundup/cgi/actions.py Sun Apr 28 18:44:05 2019 -0400 +++ b/roundup/cgi/actions.py Sat May 11 17:24:58 2019 -0400 @@ -5,11 +5,15 @@ from roundup.i18n import _ from roundup.cgi import exceptions, templating from roundup.mailgw import uidFromAddress +from roundup.rate_limit import Store, RateLimit from roundup.exceptions import Reject, RejectRaw from roundup.anypy import urllib_ from roundup.anypy.strings import StringIO import roundup.anypy.random_ as random_ +import time +from datetime import timedelta + # Also add action to client.py::Client.actions property __all__ = ['Action', 'ShowAction', 'RetireAction', 'RestoreAction', 'SearchAction', 'EditCSVAction', 'EditItemAction', 'PassResetAction', @@ -31,6 +35,7 @@ self.base = client.base self.user = client.user self.context = templating.context(client) + self.loginLimit = RateLimit(self.db.config['WEB_LOGIN_ATTEMPTS_MIN'], timedelta(seconds=60)) def handle(self): """Action handler procedure""" @@ -1226,7 +1231,34 @@ ) try: - self.verifyLogin(self.client.user, password) + # Implement rate limiting of logins by login name. + # Use prefix to prevent key collisions maybe?? + rlkey="LOGIN-" + self.client.user + limit=self.loginLimit + s=Store() + otk=self.client.db.Otk + try: + val=otk.getall(rlkey) + s.set_tat_as_string(rlkey, val['tat']) + except KeyError: + # ignore if tat not set, it's 1970-1-1 by default. + pass + # see if rate limit exceeded and we need to reject the attempt + reject=s.update(rlkey, limit) + + # Calculate a timestamp that will make OTK expire the + # unused entry 1 hour in the future + ts = time.time() - (60 * 60 * 24 * 7) + 3600 + otk.set(rlkey, tat=s.get_tat_as_string(rlkey), + __timestamp=ts) + otk.commit() + + if reject: + # User exceeded limits: find out how long to wait + status=s.status(rlkey, limit) + raise Reject(_("Logins occurring too fast. Please wait: %d seconds.")%status['Retry-After']) + else: + self.verifyLogin(self.client.user, password) except exceptions.LoginError as err: self.client.make_user_anonymous() for arg in err.args:
