forked from CGM_Public/pretix_original
[SECURITY] Support custom media URLs in CSP middleware
This commit is contained in:
@@ -7,6 +7,7 @@ from django.core.urlresolvers import get_script_prefix
|
|||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.cache import patch_vary_headers
|
from django.utils.cache import patch_vary_headers
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
from django.utils.translation import LANGUAGE_SESSION_KEY
|
from django.utils.translation import LANGUAGE_SESSION_KEY
|
||||||
from django.utils.translation.trans_real import (
|
from django.utils.translation.trans_real import (
|
||||||
@@ -165,6 +166,9 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
'/api/v1/docs/',
|
'/api/v1/docs/',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def process_request(self, request):
|
||||||
|
request.csp_nonce = get_random_string(length=32)
|
||||||
|
|
||||||
def process_response(self, request, resp):
|
def process_response(self, request, resp):
|
||||||
if settings.DEBUG and resp.status_code >= 400:
|
if settings.DEBUG and resp.status_code >= 400:
|
||||||
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
||||||
@@ -179,9 +183,9 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
# frame-src is deprecated but kept for compatibility with CSP 1.0 browsers, e.g. Safari 9
|
# frame-src is deprecated but kept for compatibility with CSP 1.0 browsers, e.g. Safari 9
|
||||||
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||||
'child-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
'child-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||||
'style-src': ["{static}"],
|
'style-src': ["{static}", "'nonce-{nonce}'"],
|
||||||
'connect-src': ["{dynamic}", "https://checkout.stripe.com"],
|
'connect-src': ["{dynamic}", "https://checkout.stripe.com"],
|
||||||
'img-src': ["{static}", "data:", "https://*.stripe.com"],
|
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
|
||||||
# form-action is not only used to match on form actions, but also on URLs
|
# form-action is not only used to match on form actions, but also on URLs
|
||||||
# form-actions redirect to. In the context of e.g. payment providers or
|
# form-actions redirect to. In the context of e.g. payment providers or
|
||||||
# single-sign-on this can be nearly anything so we cannot really restrict
|
# single-sign-on this can be nearly anything so we cannot really restrict
|
||||||
@@ -193,6 +197,9 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
|
|
||||||
staticdomain = "'self'"
|
staticdomain = "'self'"
|
||||||
dynamicdomain = "'self'"
|
dynamicdomain = "'self'"
|
||||||
|
mediadomain = "'self'"
|
||||||
|
if settings.MEDIA_URL.startswith('http'):
|
||||||
|
mediadomain += " " + settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)]
|
||||||
if settings.STATIC_URL.startswith('http'):
|
if settings.STATIC_URL.startswith('http'):
|
||||||
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
|
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
|
||||||
if settings.SITE_URL.startswith('http'):
|
if settings.SITE_URL.startswith('http'):
|
||||||
@@ -212,5 +219,6 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
dynamicdomain += " " + domain
|
dynamicdomain += " " + domain
|
||||||
|
|
||||||
if request.path not in self.CSP_EXEMPT:
|
if request.path not in self.CSP_EXEMPT:
|
||||||
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain)
|
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
|
||||||
|
media=mediadomain, nonce=request.csp_nonce)
|
||||||
return resp
|
return resp
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from urllib.parse import urljoin, urlsplit
|
||||||
|
|
||||||
import django_libsass
|
import django_libsass
|
||||||
import sass
|
import sass
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
|
from django.templatetags.static import static as _static
|
||||||
|
|
||||||
from pretix.base.models import Event
|
from pretix.base.models import Event
|
||||||
from pretix.base.services.async import ProfiledTask
|
from pretix.base.services.async import ProfiledTask
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
|
from pretix.multidomain.urlreverse import get_domain
|
||||||
|
|
||||||
logger = logging.getLogger('pretix.presale.style')
|
logger = logging.getLogger('pretix.presale.style')
|
||||||
|
|
||||||
@@ -25,10 +28,25 @@ def regenerate_css(event_id: int):
|
|||||||
sassrules.append('$brand-primary: {};'.format(event.settings.get('primary_color')))
|
sassrules.append('$brand-primary: {};'.format(event.settings.get('primary_color')))
|
||||||
sassrules.append('@import "main.scss";')
|
sassrules.append('@import "main.scss";')
|
||||||
|
|
||||||
|
def static(path):
|
||||||
|
sp = _static(path)
|
||||||
|
if not settings.MEDIA_URL.startswith("/") and sp.startswith("/"):
|
||||||
|
domain = get_domain(event.organizer)
|
||||||
|
if domain:
|
||||||
|
siteurlsplit = urlsplit(settings.SITE_URL)
|
||||||
|
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
||||||
|
domain = '%s:%d' % (domain, siteurlsplit.port)
|
||||||
|
sp = urljoin('%s://%s' % (siteurlsplit.scheme, domain), sp)
|
||||||
|
else:
|
||||||
|
sp = urljoin(settings.SITE_URL, sp)
|
||||||
|
return '"{}"'.format(sp)
|
||||||
|
|
||||||
|
cf = dict(django_libsass.CUSTOM_FUNCTIONS)
|
||||||
|
cf['static'] = static
|
||||||
css = sass.compile(
|
css = sass.compile(
|
||||||
string="\n".join(sassrules),
|
string="\n".join(sassrules),
|
||||||
include_paths=[sassdir], output_style='compressed',
|
include_paths=[sassdir], output_style='compressed',
|
||||||
custom_functions=django_libsass.CUSTOM_FUNCTIONS
|
custom_functions=cf
|
||||||
)
|
)
|
||||||
checksum = hashlib.sha1(css.encode('utf-8')).hexdigest()
|
checksum = hashlib.sha1(css.encode('utf-8')).hexdigest()
|
||||||
fname = '{}/{}/presale.{}.css'.format(
|
fname = '{}/{}/presale.{}.css'.format(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<link rel="stylesheet" type="text/x-scss" href="{% static "lightbox/css/lightbox.scss" %}" />
|
<link rel="stylesheet" type="text/x-scss" href="{% static "lightbox/css/lightbox.scss" %}" />
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% if css_file %}
|
{% if css_file %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}"/>
|
<link rel="stylesheet" type="text/css" href="{{ css_file }}" nonce="{{ request.csp_nonce }}" />
|
||||||
{% else %}
|
{% else %}
|
||||||
{% compress css %}
|
{% compress css %}
|
||||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixpresale/scss/main.scss" %}"/>
|
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixpresale/scss/main.scss" %}"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user