diff --git a/src/openforms/registrations/contrib/json_dump/config.py b/src/openforms/registrations/contrib/json_dump/config.py index 3446c9eba7..407d15e098 100644 --- a/src/openforms/registrations/contrib/json_dump/config.py +++ b/src/openforms/registrations/contrib/json_dump/config.py @@ -1,5 +1,6 @@ from typing import Required, TypedDict +from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -11,6 +12,18 @@ from openforms.utils.mixins import JsonSchemaSerializerMixin +def validate_path(v: str) -> None: + """Validate path by checking if it contains '..', which can lead to path traversal + attacks. + + :param v: path to validate + """ + if ".." in v: + raise ValidationError( + "Path cannot contain '..', as it can lead to path traversal attacks." + ) + + class JSONDumpOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serializer): service = PrimaryKeyRelatedAsChoicesField( queryset=Service.objects.filter(api_type=APITypes.orc), @@ -29,6 +42,7 @@ class JSONDumpOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serialize allow_blank=True, required=False, default="", + validators=[validate_path], ) form_variables = serializers.ListField( child=FormioVariableKeyField(), diff --git a/src/openforms/registrations/contrib/json_dump/plugin.py b/src/openforms/registrations/contrib/json_dump/plugin.py index 8726da6ce1..0e864c010c 100644 --- a/src/openforms/registrations/contrib/json_dump/plugin.py +++ b/src/openforms/registrations/contrib/json_dump/plugin.py @@ -1,6 +1,7 @@ import base64 import json +from django.core.exceptions import SuspiciousOperation from django.core.serializers.json import DjangoJSONEncoder from django.utils.translation import gettext_lazy as _ @@ -62,10 +63,10 @@ def register_submission( service = options["service"] submission.registration_result = result = {} with build_client(service) as client: - res = client.post( - options.get("relative_api_endpoint", ""), - json=data, - ) + if ".." in (path := options["relative_api_endpoint"]): + raise SuspiciousOperation("Possible path traversal detected") + + res = client.post(path, json=data) res.raise_for_status() result["api_response"] = res.json() @@ -120,6 +121,7 @@ def process_variables(submission: Submission, values: JSONObject): f"attachments ({n_attachments}) is not allowed." ) + def encode_attachment(attachment: SubmissionFileAttachment) -> str: """Encode an attachment using base64 diff --git a/src/openforms/registrations/contrib/json_dump/tests/test_backend.py b/src/openforms/registrations/contrib/json_dump/tests/test_backend.py index 514e7dceb2..dc637e86f5 100644 --- a/src/openforms/registrations/contrib/json_dump/tests/test_backend.py +++ b/src/openforms/registrations/contrib/json_dump/tests/test_backend.py @@ -1,6 +1,7 @@ from base64 import b64decode from pathlib import Path +from django.core.exceptions import SuspiciousOperation from django.test import TestCase from requests import RequestException @@ -178,3 +179,31 @@ def test_multiple_file_uploads(self): res_json = res["api_response"] self.assertEqual(res_json["data"]["values"], expected_values) + + def test_path_traversal_attack(self): + submission = SubmissionFactory.from_components( + [ + {"key": "firstName", "type": "textField"}, + {"key": "lastName", "type": "textfield"}, + ], + completed=True, + submitted_data={ + "firstName": "We Are", + "lastName": "Checking", + }, + bsn="123456789", + ) + + json_form_options = dict( + service=(ServiceFactory(api_root="http://localhost:80/")), + path="..", + form_variables=["firstName", "file", "auth_bsn"], + ) + json_plugin = JSONDumpRegistration("json_registration_plugin") + set_submission_reference(submission) + + for path in ("..", "../foo", "foo/..", "foo/../bar"): + with self.subTest(path): + json_form_options["path"] = path + with self.assertRaises(SuspiciousOperation): + json_plugin.register_submission(submission, json_form_options) diff --git a/src/openforms/registrations/contrib/json_dump/tests/test_config.py b/src/openforms/registrations/contrib/json_dump/tests/test_config.py new file mode 100644 index 0000000000..d499e4125d --- /dev/null +++ b/src/openforms/registrations/contrib/json_dump/tests/test_config.py @@ -0,0 +1,26 @@ +from django.test import TestCase + +from openforms.appointments.contrib.qmatic.tests.factories import ServiceFactory + +from ..config import JSONDumpOptions, JSONDumpOptionsSerializer + + +class JSONDumpConfigTests(TestCase): + def test_serializer_raises_validation_error_on_path_traversal(self): + service = ServiceFactory.create(api_root="https://example.com/api/v2") + + data: JSONDumpOptions = { + "service": service.pk, + "path": "", + "variables": ["now"], + } + + # Ensuring that the options are valid in the first place + serializer = JSONDumpOptionsSerializer(data=data) + self.assertTrue(serializer.is_valid()) + + for path in ("..", "../foo", "foo/..", "foo/../bar"): + with self.subTest(path): + data["path"] = path + serializer = JSONDumpOptionsSerializer(data=data) + self.assertFalse(serializer.is_valid())