From 32f304fd511a7b80af6430d09ce3b006054e948c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Tue, 13 Aug 2019 13:58:55 +0200 Subject: [PATCH] misc: add rate limiting to tracking code URL (#35386) --- debian/control | 1 + jenkins.sh | 2 +- setup.py | 1 + tests/conftest.py | 9 +++++++++ tests/test_form_pages.py | 27 +++++++++++++++++---------- wcs/forms/root.py | 12 ++++++++++++ 6 files changed, 41 insertions(+), 11 deletions(-) diff --git a/debian/control b/debian/control index 5fe865fb4..d3842b095 100644 --- a/debian/control +++ b/debian/control @@ -15,6 +15,7 @@ Depends: ${misc:Depends}, ${python:Depends}, python-hobo, graphviz, python-django-ckeditor, + python-django-ratelimit, python-feedparser, python-imaging, python-pyproj, diff --git a/jenkins.sh b/jenkins.sh index bb5707bd9..f4d9cb6aa 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -27,7 +27,7 @@ $PIP_BIN install --upgrade setuptools $PIP_BIN install --upgrade 'pytest<4.1' WebTest mock pytest-cov pyquery pytest-django $PIP_BIN install --upgrade 'pylint<1.8' # 1.8 broken (cf build #3023) $PIP_BIN install git+https://git.entrouvert.org/debian/django-ckeditor.git -$PIP_BIN install --upgrade 'Django<1.12' 'gadjo' 'pyproj' +$PIP_BIN install --upgrade 'Django<1.12' 'gadjo' 'pyproj' 'django-ratelimit<3' DJANGO_SETTINGS_MODULE=wcs.settings \ WCS_SETTINGS_FILE=tests/settings.py \ diff --git a/setup.py b/setup.py index 24122c12e..3c9e50132 100644 --- a/setup.py +++ b/setup.py @@ -109,6 +109,7 @@ setup( install_requires=[ 'gadjo>=0.53', 'django-ckeditor<=4.5.3', + 'django-ratelimit<3', 'XStatic-Leaflet', 'pyproj', ], diff --git a/tests/conftest.py b/tests/conftest.py index 783847ac0..a28cdfdc8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,3 +68,12 @@ def sms_mocking(): def http_requests(): with HttpRequestsMocking() as http_requests: yield http_requests + + +@pytest.fixture +def nocache(settings): + settings.CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + } + } diff --git a/tests/test_form_pages.py b/tests/test_form_pages.py index 5e90079ca..a60903ce2 100644 --- a/tests/test_form_pages.py +++ b/tests/test_form_pages.py @@ -55,7 +55,6 @@ def assert_equal_zip(stream1, stream2): t1, t2 = z1.read(name), z2.read(name) assert t1 == t2, 'file "%s" differs' % name - def pytest_generate_tests(metafunc): if 'pub' in metafunc.fixturenames: metafunc.parametrize('pub', ['pickle', 'sql', 'pickle-templates', 'pickle-lazy'], indirect=True) @@ -1311,7 +1310,7 @@ def get_displayed_tracking_code(resp): break return tracking_code -def test_form_tracking_code(pub): +def test_form_tracking_code(pub, nocache): formdef = create_formdef() formdef.fields = [fields.StringField(id='0', label='string')] formdef.enable_tracking_codes = True @@ -1412,8 +1411,16 @@ def test_form_tracking_code(pub): assert resp.location == 'http://example.net/test/%s' % formdata_id resp = resp.follow() +def test_form_tracking_code_rate_limit(pub): + # three errors + get_app(pub).get('/code/ABC/load', status=404) + get_app(pub).get('/code/ABC/load', status=404) + get_app(pub).get('/code/ABC/load', status=404) + # and out + get_app(pub).get('/code/ABC/load', status=403) + get_app(pub).get('/code/ABC/load', status=403) -def test_form_tracking_code_as_user(pub): +def test_form_tracking_code_as_user(pub, nocache): user = create_user(pub) formdef = create_formdef() formdef.fields = [fields.StringField(id='0', label='string')] @@ -1493,7 +1500,7 @@ def test_form_tracking_code_as_user(pub): resp = app.get('/code/%s/load' % tracking_code, headers={'User-agent': 'Googlebot'}, status=403) -def test_form_empty_tracking_code(pub): +def test_form_empty_tracking_code(pub, nocache): formdef = create_formdef() formdef.fields = [fields.StringField(id='0', label='string')] formdef.enable_tracking_codes = True @@ -1512,7 +1519,7 @@ def test_form_empty_tracking_code(pub): assert resp.location == 'http://example.net/code/%s/load' % tracking_code resp = resp.follow(status=404) -def test_form_tracking_code_email(pub, emails): +def test_form_tracking_code_email(pub, emails, nocache): formdef = create_formdef() formdef.data_class().wipe() formdef.fields = [fields.StringField(id='0', label='string'), @@ -1541,7 +1548,7 @@ def test_form_tracking_code_email(pub, emails): resp = resp.follow() assert resp.forms[1]['f0'].value == 'barfoo' -def test_form_tracking_code_remove_draft(pub): +def test_form_tracking_code_remove_draft(pub, nocache): formdef = create_formdef() formdef.fields = [fields.StringField(id='0', label='string')] formdef.enable_tracking_codes = True @@ -1587,7 +1594,7 @@ def test_form_tracking_code_remove_draft(pub): assert resp.location == 'http://example.net/' assert formdef.data_class().count() == 0 -def test_form_tracking_code_remove_empty_draft(pub): +def test_form_tracking_code_remove_empty_draft(pub, nocache): formdef = create_formdef() formdef.fields = [fields.StringField(id='0', label='string')] formdef.enable_tracking_codes = True @@ -1635,7 +1642,7 @@ def test_form_tracking_code_remove_empty_draft(pub): assert resp.location == 'http://example.net/' assert formdef.data_class().count() == 0 -def test_form_discard_draft(pub): +def test_form_discard_draft(pub, nocache): user = create_user(pub) formdef = create_formdef() @@ -1733,7 +1740,7 @@ def test_form_discard_draft(pub): resp = resp.forms[1].submit('cancel') assert [x.status for x in formdef.data_class().select()] == ['draft'] -def test_form_invalid_tracking_code(pub): +def test_form_invalid_tracking_code(pub, nocache): formdef = create_formdef() formdef.fields = [fields.StringField(id='0', label='string')] formdef.enable_tracking_codes = True @@ -1785,7 +1792,7 @@ def test_form_invalid_tracking_code(pub): assert resp.location == 'http://example.net/code/%s/load' % code.id resp = resp.follow(status=404) -def test_form_tracking_code_as_variable(pub): +def test_form_tracking_code_as_variable(pub, nocache): formdef = create_formdef() formdef.fields = [fields.PageField(id='0', label='1st page', type='page'), fields.StringField(id='1', label='string'), diff --git a/wcs/forms/root.py b/wcs/forms/root.py index 951d0fe15..c3333938a 100644 --- a/wcs/forms/root.py +++ b/wcs/forms/root.py @@ -29,6 +29,8 @@ from django.utils.http import quote from django.utils.six import StringIO from django.utils.safestring import mark_safe +import ratelimit.utils + from quixote import (get_publisher, get_request, get_response, get_session, get_session_manager, redirect) from quixote.directory import Directory, AccessControlled @@ -148,6 +150,16 @@ class TrackingCodeDirectory(Directory): return r.getvalue() def load(self): + rate_limit = get_publisher().get_site_option('rate-limit') or '3/s' + if rate_limit != 'none': + ratelimited = ratelimit.utils.is_ratelimited( + request=get_request().django_request, + group='trackingcode', + key='ip', + rate=rate_limit, + increment=True) + if ratelimited: + raise errors.AccessForbiddenError('rate limit reached') try: tracking_code = get_publisher().tracking_code_class.get(self.code) if tracking_code.formdata_id is None: