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
This commit is contained in:
Raphael Michel
2024-07-31 13:11:38 +02:00
committed by GitHub
parent 78cfbd6460
commit 13720e731e
12 changed files with 68 additions and 20 deletions

View File

@@ -2,7 +2,7 @@
{% load compress %} {% load compress %}
{% load i18n %} {% load i18n %}
{% compress js %} {% compress js file paypal %}
<script type="text/javascript" src="{% static "pretixplugins/paypal2/pretix-paypal.js" %}"></script> <script type="text/javascript" src="{% static "pretixplugins/paypal2/pretix-paypal.js" %}"></script>
{% endcompress %} {% endcompress %}

View File

@@ -185,6 +185,10 @@ class XHRView(View):
class PayView(PaypalOrderView, TemplateView): class PayView(PaypalOrderView, TemplateView):
template_name = '' 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): def get(self, request, *args, **kwargs):
if self.payment.state != OrderPayment.PAYMENT_STATE_CREATED: if self.payment.state != OrderPayment.PAYMENT_STATE_CREATED:
return self._redirect_to_order() return self._redirect_to_order()

View File

@@ -2,10 +2,10 @@
{% load compress %} {% load compress %}
{% load i18n %} {% load i18n %}
{% compress js %} {% compress js file stripe %}
<script type="text/javascript" src="{% static "pretixplugins/stripe/pretix-stripe.js" %}"></script> <script type="text/javascript" src="{% static "pretixplugins/stripe/pretix-stripe.js" %}"></script>
{% endcompress %} {% endcompress %}
{% compress css %} {% compress css file stripe %}
<link type="text/css" rel="stylesheet" href="{% static "pretixplugins/stripe/pretix-stripe.css" %}"> <link type="text/css" rel="stylesheet" href="{% static "pretixplugins/stripe/pretix-stripe.css" %}">
{% endcompress %} {% endcompress %}
{% if testmode %} {% if testmode %}

View File

@@ -1263,6 +1263,7 @@ class PaymentStep(CartMixin, TemplateFlowStep):
def post(self, request): def post(self, request):
self.request = request self.request = request
self.request.pci_dss_payment_page = True
if "remove_payment" in request.POST: if "remove_payment" in request.POST:
self._remove_payment(request.POST["remove_payment"]) self._remove_payment(request.POST["remove_payment"])
@@ -1427,6 +1428,10 @@ class PaymentStep(CartMixin, TemplateFlowStep):
return True return True
def get(self, request):
self.request.pci_dss_payment_page = True
return super().get(request)
class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
priority = 1001 priority = 1001

View File

@@ -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. ``request`` and are expected to return plain HTML.
As with all plugin signals, the ``sender`` keyword argument will contain the event. 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() 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. ``request`` and are expected to return plain HTML.
As with all plugin signals, the ``sender`` keyword argument will contain the event. 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() footer_link = EventPluginSignal()

View File

@@ -8,7 +8,7 @@
<html{% if rtl %} dir="rtl" class="rtl"{% endif %} lang="{{ html_locale }}"> <html{% if rtl %} dir="rtl" class="rtl"{% endif %} lang="{{ html_locale }}">
<head> <head>
<title>{% block thetitle %}{% endblock %}</title> <title>{% block thetitle %}{% endblock %}</title>
{% compress css %} {% compress css file presale %}
<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" %}"/>
{% endcompress %} {% endcompress %}
{% if css_theme %} {% if css_theme %}
@@ -92,7 +92,7 @@
<script src="{% statici18n request.LANGUAGE_CODE %}"></script> <script src="{% statici18n request.LANGUAGE_CODE %}"></script>
{% endif %} {% endif %}
{% if request.session.iframe_session %} {% if request.session.iframe_session %}
{% compress js %} {% compress js file iframeresizer %}
<script type="text/javascript" src="{% static "iframeresizer/iframeResizer.contentWindow.js" %}"></script> <script type="text/javascript" src="{% static "iframeresizer/iframeResizer.contentWindow.js" %}"></script>
{% endcompress %} {% endcompress %}
{% endif %} {% endif %}

View File

@@ -50,19 +50,15 @@
<ul class="list-group"> <ul class="list-group">
{% for payment, rendered_block in payments %} {% for payment, rendered_block in payments %}
<li class="list-group-item payment"> <li class="list-group-item payment">
{% if payments|length > 1 %} <div class="row">
<div class="row"> <div class="{% if payments|length > 1 %}col-sm-10 {% endif %}col-xs-12">
<div class="col-sm-10 col-xs-12"> <h4 {% if payments|length == 1 %}class="sr-only"{% endif %}>{{ payment.provider_name }}</h4>
<h4>{{ payment.provider_name }}</h4> {{ rendered_block }}
{{ rendered_block }}
</div>
<div class="col-sm-2 col-xs-12 text-right">
<h4>{{ payment.payment_amount|money:request.event.currency }}</h4>
</div>
</div> </div>
{% else %} <div class="col-sm-2 col-xs-12 text-right {% if payments|length == 1 %}sr-only{% endif %}">
{{ rendered_block }} <h4>{{ payment.payment_amount|money:request.event.currency }}</h4>
{% endif %} </div>
</div>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@@ -1,6 +1,6 @@
{% load static %} {% load static %}
{% load compress %} {% load compress %}
{% compress js %} {% compress js file walletdetection %}
<script type="text/javascript" src="{% static "pretixpresale/js/walletdetection.js" %}"></script> <script type="text/javascript" src="{% static "pretixpresale/js/walletdetection.js" %}"></script>
{% endcompress %} {% endcompress %}

View File

@@ -1,6 +1,6 @@
{% load static %} {% load static %}
{% load compress %} {% load compress %}
{% compress js %} {% compress js file presale %}
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script> <script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
<script type="text/javascript" src="{% static "moment/moment-with-locales.js" %}"></script> <script type="text/javascript" src="{% static "moment/moment-with-locales.js" %}"></script>
<script type="text/javascript" src="{% static "moment/moment-timezone-with-data-1970-2030.js" %}"></script> <script type="text/javascript" src="{% static "moment/moment-timezone-with-data-1970-2030.js" %}"></script>

View File

@@ -359,6 +359,7 @@ class OrderPaymentStart(EventViewMixin, OrderDetailMixin, TemplateView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.request = request self.request = request
self.request.pci_dss_payment_page = True
if not self.order: if not self.order:
raise Http404(_('Unknown order code or not authorized to access this 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) 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): def dispatch(self, request, *args, **kwargs):
self.request = request self.request = request
self.request.pci_dss_payment_page = True
if not self.order: if not self.order:
raise Http404(_('Unknown order code or not authorized to access this 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: if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) or self.order._can_be_paid() is not True:

View File

@@ -68,7 +68,7 @@ class BaseCheckoutTestCase:
self.event = Event.objects.create( self.event = Event.objects.create(
organizer=self.orga, name='30C3', slug='30c3', organizer=self.orga, name='30C3', slug='30c3',
date_from=datetime.datetime(now().year + 1, 12, 26, tzinfo=datetime.timezone.utc), 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 live=True
) )
self.tr19 = self.event.tax_rules.create(rate=19) 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) self._set_session('invoice_address', ia.pk)
return ia 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): def test_empty_cart(self):
response = self.client.get('/%s/%s/checkout/start' % (self.orga.slug, self.event.slug), follow=True) 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), self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),

View File

@@ -26,6 +26,7 @@ from pretix.base.signals import (
register_payment_providers, register_sales_channel_types, register_payment_providers, register_sales_channel_types,
register_ticket_outputs, register_ticket_outputs,
) )
from pretix.presale.signals import html_head
@receiver(register_ticket_outputs, dispatch_uid="output_dummy") @receiver(register_ticket_outputs, dispatch_uid="output_dummy")
@@ -61,3 +62,11 @@ class FoobarSalesChannel(SalesChannelType):
@receiver(register_sales_channel_types, dispatch_uid="sc_dummy") @receiver(register_sales_channel_types, dispatch_uid="sc_dummy")
def register_sc(sender, **kwargs): def register_sc(sender, **kwargs):
return [FoobarSalesChannel, FoobazSalesChannel] 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 "<script>alert('BAD TRACKING SCRIPT')</script>"