From 959e926a6740fa5c9bb68988edd2ff59fd6d3a74 Mon Sep 17 00:00:00 2001 From: Richard Schreiber Date: Mon, 2 Mar 2026 12:28:47 +0100 Subject: [PATCH] API: validate payment_info (#5944) * API: validate payment_info * improve dict-check * Apply suggestions from code review Co-authored-by: Raphael Michel --------- Co-authored-by: Raphael Michel --- src/pretix/api/serializers/order.py | 13 +++++++++++ src/tests/api/test_order_create.py | 35 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 4fb4dce059..33eba51d2c 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -19,6 +19,7 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +import json import logging import os from collections import Counter, defaultdict @@ -1215,6 +1216,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer): raise ValidationError('The given payment provider is not known.') return pp + def validate_payment_info(self, info): + if info: + try: + obj = json.loads(info) + except ValueError: + raise ValidationError('payment_info must be valid JSON.') + + if not isinstance(obj, dict): + # only objects are allowed + raise ValidationError('payment_info must be a JSON object.') + return info + def validate_expires(self, expires): if expires < now(): raise ValidationError('Expiration date must be in the future.') diff --git a/src/tests/api/test_order_create.py b/src/tests/api/test_order_create.py index f3f1d7e3ef..e69b1afa78 100644 --- a/src/tests/api/test_order_create.py +++ b/src/tests/api/test_order_create.py @@ -895,6 +895,41 @@ def test_order_create_payment_info_optional(token_client, organizer, event, item assert json.loads(p.info) == res['payment_info'] +@pytest.mark.django_db +def test_order_create_payment_info_valid_object(token_client, organizer, event, item, quota, question): + res = copy.deepcopy(ORDER_CREATE_PAYLOAD) + res['positions'][0]['item'] = item.pk + res['positions'][0]['answers'][0]['question'] = question.pk + + res["payment_info"] = [{"should": "fail"}] + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/orders/'.format( + organizer.slug, event.slug + ), format='json', data=res + ) + assert resp.status_code == 400 + + res['payment_info'] = { + 'foo': { + 'bar': [1, 2], + 'test': False + } + } + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/orders/'.format( + organizer.slug, event.slug + ), format='json', data=res + ) + assert resp.status_code == 201 + with scopes_disabled(): + o = Order.objects.get(code=resp.data['code']) + + p = o.payments.first() + assert p.provider == "banktransfer" + assert p.amount == o.total + assert json.loads(p.info) == res['payment_info'] + + @pytest.mark.django_db def test_order_create_position_secret_optional(token_client, organizer, event, item, quota, question): res = copy.deepcopy(ORDER_CREATE_PAYLOAD)