From 8a903f21aef782ec8ccd455bd10b976d65e13a8c Mon Sep 17 00:00:00 2001 From: Martin Gross Date: Mon, 17 Jul 2023 11:15:12 +0200 Subject: [PATCH] Stripe/Middleware: Move CSP to signal (#3465) --- src/pretix/base/middleware.py | 20 ++++++++++----- src/pretix/plugins/stripe/signals.py | 37 +++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/pretix/base/middleware.py b/src/pretix/base/middleware.py index 74721fb589..80eaa2951f 100644 --- a/src/pretix/base/middleware.py +++ b/src/pretix/base/middleware.py @@ -26,7 +26,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from django.conf import settings from django.http import Http404, HttpRequest, HttpResponse from django.middleware.common import CommonMiddleware -from django.urls import get_script_prefix +from django.urls import get_script_prefix, resolve from django.utils import timezone, translation from django.utils.cache import patch_vary_headers from django.utils.deprecation import MiddlewareMixin @@ -230,6 +230,8 @@ class SecurityMiddleware(MiddlewareMixin): ) def process_response(self, request, resp): + url = resolve(request.path_info) + if settings.DEBUG and resp.status_code >= 400: # Don't use CSP on debug error page as it breaks of Django's fancy error # pages @@ -249,20 +251,26 @@ class SecurityMiddleware(MiddlewareMixin): h = { 'default-src': ["{static}"], - 'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com', 'https://pay.google.com'], + 'script-src': ['{static}'], 'object-src': ["'none'"], - 'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'], + 'frame-src': ['{static}'], 'style-src': ["{static}", "{media}"], - 'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"], - 'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"] + img_src, + 'connect-src': ["{dynamic}", "{media}"], + 'img-src': ["{static}", "{media}", "data:"] + img_src, 'font-src': ["{static}"], 'media-src': ["{static}", "data:"], # 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 - # 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 # this. However, we'll restrict it to HTTPS. 'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []), } + # Only include pay.google.com for wallet detection purposes on the Payment selection page + if ( + url.url_name == "event.order.pay.change" or + (url.url_name == "event.checkout" and url.kwargs['step'] == "payment") + ): + h['script-src'].append('https://pay.google.com') if settings.LOG_CSP: h['report-uri'] = ["/csp_report/"] if 'Content-Security-Policy' in resp: diff --git a/src/pretix/plugins/stripe/signals.py b/src/pretix/plugins/stripe/signals.py index 88b2efa2b2..2d700b0566 100644 --- a/src/pretix/plugins/stripe/signals.py +++ b/src/pretix/plugins/stripe/signals.py @@ -24,18 +24,22 @@ from collections import OrderedDict from django import forms from django.dispatch import receiver +from django.http import HttpRequest from django.template.loader import get_template from django.urls import resolve, reverse from django.utils.translation import gettext_lazy as _ +from paypalhttp import HttpResponse from pretix.base.forms import SecretKeySettingsField +from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp from pretix.base.settings import settings_hierarkey from pretix.base.signals import ( logentry_display, register_global_settings, register_payment_providers, ) from pretix.control.signals import nav_organizer from pretix.plugins.stripe.forms import StripeKeyValidator -from pretix.presale.signals import html_head +from pretix.plugins.stripe.payment import StripeMethod +from pretix.presale.signals import html_head, process_response @receiver(register_payment_providers, dispatch_uid="payment_stripe") @@ -178,3 +182,34 @@ def nav_o(sender, request, organizer, **kwargs): 'active': 'settings.connect' in url.url_name, }] return [] + + +@receiver(signal=process_response, dispatch_uid="stripe_middleware_resp") +def signal_process_response(sender, request: HttpRequest, response: HttpResponse, **kwargs): + provider = StripeMethod(sender) + url = resolve(request.path_info) + + if provider.settings.get('_enabled', as_type=bool) and ( + url.url_name == "event.order.pay.change" or + url.url_name == "event.order.pay" or + (url.url_name == "event.checkout" and url.kwargs['step'] == "payment") or + (url.namespace == "plugins:stripe" and url.url_name in ["sca", "sca.return"]) + ): + if 'Content-Security-Policy' in response: + h = _parse_csp(response['Content-Security-Policy']) + else: + h = {} + + # https://stripe.com/docs/security/guide#content-security-policy + csps = { + 'connect-src': ['https://api.stripe.com'], + 'frame-src': ['https://js.stripe.com', 'https://hooks.stripe.com'], + 'script-src': ['https://js.stripe.com'], + } + + _merge_csp(h, csps) + + if h: + response['Content-Security-Policy'] = _render_csp(h) + + return response