diff --git a/src/pretix/base/middleware.py b/src/pretix/base/middleware.py index d362b5a87f..def5dd60f5 100644 --- a/src/pretix/base/middleware.py +++ b/src/pretix/base/middleware.py @@ -192,6 +192,7 @@ class SecurityMiddleware(MiddlewareMixin): # single-sign-on this can be nearly anything so we cannot really restrict # this. However, we'll restrict it to HTTPS. 'form-action': ["{dynamic}", "https:"], + 'report-uri': ["/csp_report/"], } if 'Content-Security-Policy' in resp: _merge_csp(h, _parse_csp(resp['Content-Security-Policy'])) diff --git a/src/pretix/base/validators.py b/src/pretix/base/validators.py index d5f8c3fa60..da7d87380d 100644 --- a/src/pretix/base/validators.py +++ b/src/pretix/base/validators.py @@ -32,6 +32,7 @@ class EventSlugBlacklistValidator(BlacklistValidator): '__debug__', 'api', 'events', + 'csp_report', ] @@ -51,4 +52,5 @@ class OrganizerSlugBlacklistValidator(BlacklistValidator): '__debug__', 'about', 'api', + 'csp_report', ] diff --git a/src/pretix/base/views/csp.py b/src/pretix/base/views/csp.py new file mode 100644 index 0000000000..b4a72ad011 --- /dev/null +++ b/src/pretix/base/views/csp.py @@ -0,0 +1,25 @@ +import json +import logging + +from django.http import ( + HttpResponseBadRequest, HttpResponse) +from django.views.decorators.csrf import csrf_exempt + +logger = logging.getLogger('pretix.security.csp') + + +@csrf_exempt +def csp_report(request): + try: + body = json.loads(request.body.decode()) + logger.warning( + 'CSP violation at {r[document-uri]}\n' + 'Referer: {r[referrer]}\n' + 'Blocked: {r[blocked-uri]}\n' + 'Violated: {r[violated-directive]}\n' + 'Original polity: {r[original-policy]}'.format(r=body['csp-report']) + ) + except (ValueError, KeyError) as e: + logger.exception('CSP report failed') + return HttpResponseBadRequest() + return HttpResponse() diff --git a/src/pretix/settings.py b/src/pretix/settings.py index b7bfb124f9..4d8c788d5e 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -451,6 +451,12 @@ LOGGING = { 'class': 'logging.StreamHandler', 'formatter': 'default' }, + 'csp_file': { + 'level': loglevel, + 'class': 'logging.FileHandler', + 'filename': os.path.join(LOG_DIR, 'csp.log'), + 'formatter': 'default' + }, 'file': { 'level': loglevel, 'class': 'logging.FileHandler', @@ -474,6 +480,11 @@ LOGGING = { 'level': loglevel, 'propagate': True, }, + 'pretix.security.csp': { + 'handlers': ['csp_file'], + 'level': loglevel, + 'propagate': False, + }, 'django.security': { 'handlers': ['file', 'console', 'mail_admins'], 'level': loglevel, diff --git a/src/pretix/urls.py b/src/pretix/urls.py index 9988fd64aa..bc4001e462 100644 --- a/src/pretix/urls.py +++ b/src/pretix/urls.py @@ -5,7 +5,7 @@ from django.views.generic import RedirectView import pretix.control.urls import pretix.presale.urls -from .base.views import cachedfiles, health, js_catalog, metrics, redirect +from .base.views import cachedfiles, health, js_catalog, metrics, redirect, csp base_patterns = [ url(r'^download/(?P[^/]+)/$', cachedfiles.DownloadView.as_view(), @@ -16,6 +16,7 @@ base_patterns = [ url(r'^jsi18n/(?P[a-zA-Z-_]+)/$', js_catalog.js_catalog, name='javascript-catalog'), url(r'^metrics$', metrics.serve_metrics, name='metrics'), + url(r'^csp_report/$', csp.csp_report, name='csp.report'), url(r'^api/v1/', include('pretix.api.urls', namespace='api-v1')), url(r'^api/$', RedirectView.as_view(url='/api/v1/'), name='redirect-api-version') ]