add some rate limiting to the password reset functionality
This commit is contained in:
@@ -11,6 +11,7 @@ import unittest
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
@@ -89,6 +90,23 @@ class ResetPasswordTests(TestCase):
|
||||
'value': "('registration/password_reset_done.html', [])",
|
||||
})
|
||||
|
||||
@patch('student.views.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True))
|
||||
def test_password_reset_ratelimited(self):
|
||||
""" Try (and fail) resetting password 30 times in a row on an non-existant email address """
|
||||
cache.clear()
|
||||
|
||||
for i in xrange(30):
|
||||
good_req = self.request_factory.post('/password_reset/', {'email': 'thisdoesnotexist@foo.com'})
|
||||
good_resp = password_reset(good_req)
|
||||
self.assertEquals(good_resp.status_code, 200)
|
||||
|
||||
# then the rate limiter should kick in and give a HttpForbidden response
|
||||
bad_req = self.request_factory.post('/password_reset/', {'email': 'thisdoesnotexist@foo.com'})
|
||||
bad_resp = password_reset(bad_req)
|
||||
self.assertEquals(bad_resp.status_code, 403)
|
||||
|
||||
cache.clear()
|
||||
|
||||
@unittest.skipIf(
|
||||
settings.FEATURES.get('DISABLE_RESET_EMAIL_TEST', False),
|
||||
dedent("""
|
||||
|
||||
@@ -72,6 +72,7 @@ import track.views
|
||||
from dogapi import dog_stats_api
|
||||
|
||||
from util.json_request import JsonResponse
|
||||
from util.bad_request_rate_limiter import BadRequestRateLimiter
|
||||
|
||||
from microsite_configuration.middleware import MicrositeConfiguration
|
||||
|
||||
@@ -86,7 +87,6 @@ AUDIT_LOG = logging.getLogger("audit")
|
||||
Article = namedtuple('Article', 'title url author image deck publication publish_date')
|
||||
ReverifyInfo = namedtuple('ReverifyInfo', 'course_id course_name course_number date status display') # pylint: disable=C0103
|
||||
|
||||
|
||||
def csrf_token(context):
|
||||
"""A csrf token that can be included in a form."""
|
||||
csrf_token = context.get('csrf_token', '')
|
||||
@@ -1345,12 +1345,23 @@ def password_reset(request):
|
||||
if request.method != "POST":
|
||||
raise Http404
|
||||
|
||||
# Add some rate limiting here by re-using the RateLimitMixin as a helper class
|
||||
limiter = BadRequestRateLimiter()
|
||||
if limiter.is_rated_limit_exceeded(request):
|
||||
AUDIT_LOG.warning("Rate limit exceeded in password_reset")
|
||||
return HttpResponseForbidden()
|
||||
|
||||
form = PasswordResetFormNoActive(request.POST)
|
||||
if form.is_valid():
|
||||
form.save(use_https=request.is_secure(),
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
request=request,
|
||||
domain_override=request.get_host())
|
||||
else:
|
||||
# bad user? tick the rate limiter counter
|
||||
AUDIT_LOG.info("Bad password_reset user passed in.")
|
||||
limiter.tick_bad_request_counter(request)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'value': render_to_string('registration/password_reset_done.html', {}),
|
||||
|
||||
23
common/djangoapps/util/bad_request_rate_limiter.py
Normal file
23
common/djangoapps/util/bad_request_rate_limiter.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
A utility class which wraps the RateLimitMixin 3rd party class to do bad request counting
|
||||
which can be used for rate limiting
|
||||
"""
|
||||
from ratelimitbackend.backends import RateLimitMixin
|
||||
|
||||
class BadRequestRateLimiter(RateLimitMixin):
|
||||
"""
|
||||
Use the 3rd party RateLimitMixin to help do rate limiting on the Password Reset flows
|
||||
"""
|
||||
|
||||
def is_rated_limit_exceeded(self, request):
|
||||
"""
|
||||
Returns if the client has been rated limited
|
||||
"""
|
||||
counts = self.get_counters(request)
|
||||
return sum(counts.values()) >= self.requests
|
||||
|
||||
def tick_bad_request_counter(self, request):
|
||||
"""
|
||||
Ticks any counters used to compute when rate limt has been reached
|
||||
"""
|
||||
self.cache_incr(self.get_cache_key(request))
|
||||
Reference in New Issue
Block a user