From 13720e731e4a4bc16d3c59b1278105b51a8f4fa0 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 31 Jul 2024 13:11:38 +0200 Subject: [PATCH] Easier PCI DSS compliance for payment pages (#4273) * Assign names to compressed scripts * Make PCI-relevant pages detectable * Make payment summary markup more consistant to easy work in tracking plugin * Add docs note --- .../pretixplugins/paypal2/presale_head.html | 2 +- src/pretix/plugins/paypal2/views.py | 4 ++++ .../pretixplugins/stripe/presale_head.html | 4 ++-- src/pretix/presale/checkoutflow.py | 5 ++++ src/pretix/presale/signals.py | 10 ++++++++ .../presale/templates/pretixpresale/base.html | 4 ++-- .../pretixpresale/event/checkout_confirm.html | 20 +++++++--------- .../event/fragment_walletdetection_head.html | 2 +- .../templates/pretixpresale/fragment_js.html | 2 +- src/pretix/presale/views/order.py | 2 ++ src/tests/presale/test_checkout.py | 24 ++++++++++++++++++- src/tests/testdummy/signals.py | 9 +++++++ 12 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/pretix/plugins/paypal2/templates/pretixplugins/paypal2/presale_head.html b/src/pretix/plugins/paypal2/templates/pretixplugins/paypal2/presale_head.html index 35ea3046d6..f3d0b4cfe8 100644 --- a/src/pretix/plugins/paypal2/templates/pretixplugins/paypal2/presale_head.html +++ b/src/pretix/plugins/paypal2/templates/pretixplugins/paypal2/presale_head.html @@ -2,7 +2,7 @@ {% load compress %} {% load i18n %} -{% compress js %} +{% compress js file paypal %} {% endcompress %} diff --git a/src/pretix/plugins/paypal2/views.py b/src/pretix/plugins/paypal2/views.py index 2245d82b80..a59a83daaf 100644 --- a/src/pretix/plugins/paypal2/views.py +++ b/src/pretix/plugins/paypal2/views.py @@ -185,6 +185,10 @@ class XHRView(View): class PayView(PaypalOrderView, TemplateView): template_name = '' + def dispatch(self, request, *args, **kwargs): + self.request.pci_dss_payment_page = True + return super().dispatch(request, *args, **kwargs) + def get(self, request, *args, **kwargs): if self.payment.state != OrderPayment.PAYMENT_STATE_CREATED: return self._redirect_to_order() diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/presale_head.html b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/presale_head.html index b2d68e1877..40537aa609 100644 --- a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/presale_head.html +++ b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/presale_head.html @@ -2,10 +2,10 @@ {% load compress %} {% load i18n %} -{% compress js %} +{% compress js file stripe %} {% endcompress %} -{% compress css %} +{% compress css file stripe %} {% endcompress %} {% if testmode %} diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index a0afcb8ca1..7128e18730 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -1263,6 +1263,7 @@ class PaymentStep(CartMixin, TemplateFlowStep): def post(self, request): self.request = request + self.request.pci_dss_payment_page = True if "remove_payment" in request.POST: self._remove_payment(request.POST["remove_payment"]) @@ -1427,6 +1428,10 @@ class PaymentStep(CartMixin, TemplateFlowStep): return True + def get(self, request): + self.request.pci_dss_payment_page = True + return super().get(request) + class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): priority = 1001 diff --git a/src/pretix/presale/signals.py b/src/pretix/presale/signals.py index 97bbd7e06c..920466adc5 100644 --- a/src/pretix/presale/signals.py +++ b/src/pretix/presale/signals.py @@ -78,6 +78,11 @@ of every page in the frontend. You will get the request as the keyword argument ``request`` and are expected to return plain HTML. As with all plugin signals, the ``sender`` keyword argument will contain the event. + +**Note:** If PCI DSS compliance is important to you and you keep an inventory according to +rule 6.4.3 of PCI DSS, all plugins that are not required to load on a payment page should +not return additional JavaScripts if ``getattr(request, 'pci_dss_payment_page', False)`` +is ``True``. """ seatingframe_html_head = EventPluginSignal() @@ -112,6 +117,11 @@ of every page in the frontend. You will get the request as the keyword argument ``request`` and are expected to return plain HTML. As with all plugin signals, the ``sender`` keyword argument will contain the event. + +**Note:** If PCI DSS compliance is important to you and you keep an inventory according to +rule 6.4.3 of PCI DSS, all plugins that are not required to load on a payment page should +not return additional JavaScripts if ``getattr(request, 'pci_dss_payment_page', False)`` +is ``True``. """ footer_link = EventPluginSignal() diff --git a/src/pretix/presale/templates/pretixpresale/base.html b/src/pretix/presale/templates/pretixpresale/base.html index 4c86ec626b..ba10209a13 100644 --- a/src/pretix/presale/templates/pretixpresale/base.html +++ b/src/pretix/presale/templates/pretixpresale/base.html @@ -8,7 +8,7 @@ {% block thetitle %}{% endblock %} - {% compress css %} + {% compress css file presale %} {% endcompress %} {% if css_theme %} @@ -92,7 +92,7 @@ {% endif %} {% if request.session.iframe_session %} - {% compress js %} + {% compress js file iframeresizer %} {% endcompress %} {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html b/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html index f82c2f88b1..b74d399aac 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html @@ -50,19 +50,15 @@ diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_walletdetection_head.html b/src/pretix/presale/templates/pretixpresale/event/fragment_walletdetection_head.html index cecf2fd5ce..b17576a6b8 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_walletdetection_head.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_walletdetection_head.html @@ -1,6 +1,6 @@ {% load static %} {% load compress %} -{% compress js %} +{% compress js file walletdetection %} {% endcompress %} \ No newline at end of file diff --git a/src/pretix/presale/templates/pretixpresale/fragment_js.html b/src/pretix/presale/templates/pretixpresale/fragment_js.html index 2a39c384cc..f79c396dae 100644 --- a/src/pretix/presale/templates/pretixpresale/fragment_js.html +++ b/src/pretix/presale/templates/pretixpresale/fragment_js.html @@ -1,6 +1,6 @@ {% load static %} {% load compress %} -{% compress js %} +{% compress js file presale %} diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index 317a6702a1..7aea64c778 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -359,6 +359,7 @@ class OrderPaymentStart(EventViewMixin, OrderDetailMixin, TemplateView): def dispatch(self, request, *args, **kwargs): self.request = request + self.request.pci_dss_payment_page = True if not self.order: raise Http404(_('Unknown order code or not authorized to access this order.')) if (self.order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) @@ -553,6 +554,7 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView): def dispatch(self, request, *args, **kwargs): self.request = request + self.request.pci_dss_payment_page = True if not self.order: raise Http404(_('Unknown order code or not authorized to access this order.')) if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) or self.order._can_be_paid() is not True: diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index ba7ea8ebca..129af4a14a 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -68,7 +68,7 @@ class BaseCheckoutTestCase: self.event = Event.objects.create( organizer=self.orga, name='30C3', slug='30c3', date_from=datetime.datetime(now().year + 1, 12, 26, tzinfo=datetime.timezone.utc), - plugins='pretix.plugins.stripe,pretix.plugins.banktransfer', + plugins='pretix.plugins.stripe,pretix.plugins.banktransfer,tests.testdummy', live=True ) self.tr19 = self.event.tax_rules.create(rate=19) @@ -151,6 +151,28 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase): self._set_session('invoice_address', ia.pk) return ia + def test_pci_page(self): + with scopes_disabled(): + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + + r = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug)) + assert b'TRACKING SCRIPT' in r.content + + payment_r = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + 'is_business': 'business', + 'company': 'Foo', + 'name': 'Bar', + 'street': 'Baz', + 'zipcode': '1234', + 'city': 'Here', + 'country': 'AT', + 'email': 'admin@localhost' + }, follow=True) + assert b'TRACKING SCRIPT' not in payment_r.content + def test_empty_cart(self): response = self.client.get('/%s/%s/checkout/start' % (self.orga.slug, self.event.slug), follow=True) self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug), diff --git a/src/tests/testdummy/signals.py b/src/tests/testdummy/signals.py index ccc163dfa0..5a2650341e 100644 --- a/src/tests/testdummy/signals.py +++ b/src/tests/testdummy/signals.py @@ -26,6 +26,7 @@ from pretix.base.signals import ( register_payment_providers, register_sales_channel_types, register_ticket_outputs, ) +from pretix.presale.signals import html_head @receiver(register_ticket_outputs, dispatch_uid="output_dummy") @@ -61,3 +62,11 @@ class FoobarSalesChannel(SalesChannelType): @receiver(register_sales_channel_types, dispatch_uid="sc_dummy") def register_sc(sender, **kwargs): return [FoobarSalesChannel, FoobazSalesChannel] + + +@receiver(html_head, dispatch_uid="dummy_html_head") +def html_head_presale(sender, request=None, **kwargs): + if getattr(request, 'pci_dss_payment_page', False): + # No tracking scripts on PCI DSS relevant payment pages + return "" + return ""