From 1902b7d9a062ab101a6e2a1554e5d1c35f7f2179 Mon Sep 17 00:00:00 2001 From: Mervi Tyczynska Date: Fri, 1 May 2026 16:24:43 +0100 Subject: [PATCH] Allow sanitise_content_for arg in send_email_notification endpoint. So that seervice users can tell us to sanitise content for specific placeholders. This is part of the work to mitigate against the placeholder injection vulnerability. Also add sanitised_content attribute to the response schema - this is new response attribute where we tell service users when a content they told us to sanitise was actually altered as a result. --- integration_test/integration_tests.py | 4 +++- .../schemas/v2/notification_schemas.py | 8 ++++++-- notifications_python_client/notifications.py | 3 +++ .../test_notifications_api_client.py | 19 +++++++++++++++++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/integration_test/integration_tests.py b/integration_test/integration_tests.py index d312c49..9d4be7c 100644 --- a/integration_test/integration_tests.py +++ b/integration_test/integration_tests.py @@ -60,9 +60,11 @@ def send_email_notification_test_response(python_client, reply_to=None): personalisation=personalisation, one_click_unsubscribe_url=one_click_unsubscribe_url, email_reply_to_id=email_reply_to_id, + sanitise_content_for=["name"], ) validate(response, post_email_response) - assert unique_name in response["content"]["body"] # check placeholders are replaced + # check placeholders are replaced and sanitised: + assert unique_name.replace("-", "\\-") in response["content"]["body"] return response["id"] diff --git a/integration_test/schemas/v2/notification_schemas.py b/integration_test/schemas/v2/notification_schemas.py index 7fb3b5e..65739ce 100644 --- a/integration_test/schemas/v2/notification_schemas.py +++ b/integration_test/schemas/v2/notification_schemas.py @@ -115,7 +115,7 @@ }, "required": ["id", "content", "uri", "template"], } - +# TODO: this doesn't seem to be used anywhere, do we still need it? post_email_request = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "POST email notification schema", @@ -128,6 +128,7 @@ "email_reply_to_id": uuid, "personalisation": personalisation, "one_click_unsubscribe_url": https_url, + "sanitise_content_for": {"type": "array", "items": {"type": "string"}}, }, "required": ["email_address", "template_id"], } @@ -156,8 +157,9 @@ "content": email_content, "uri": {"type": "string"}, "template": template, + "sanitised_content": {"type": "object"}, }, - "required": ["id", "content", "uri", "template"], + "required": ["id", "content", "uri", "template", "sanitised_content"], } post_letter_request = { @@ -217,6 +219,7 @@ def create_post_sms_response_from_notification(notification, body, from_number, } +# TODO: we don't seem to be using this, is it needed? def create_post_email_response_from_notification(notification, content, subject, email_from, url_root): return { "id": notification.id, @@ -224,6 +227,7 @@ def create_post_email_response_from_notification(notification, content, subject, "content": {"from_email": email_from, "body": content, "subject": subject}, "uri": f"{url_root}/v2/notifications/{str(notification.id)}", "template": __create_template_from_notification(notification=notification, url_root=url_root), + "sanitised_content": {}, } diff --git a/notifications_python_client/notifications.py b/notifications_python_client/notifications.py index 7485ee9..4478d9e 100644 --- a/notifications_python_client/notifications.py +++ b/notifications_python_client/notifications.py @@ -29,6 +29,7 @@ def send_email_notification( reference=None, email_reply_to_id=None, one_click_unsubscribe_url=None, + sanitise_content_for=None, ): notification = {"email_address": email_address, "template_id": template_id} if personalisation: @@ -39,6 +40,8 @@ def send_email_notification( notification.update({"email_reply_to_id": email_reply_to_id}) if one_click_unsubscribe_url: notification.update({"one_click_unsubscribe_url": one_click_unsubscribe_url}) + if sanitise_content_for: + notification.update({"sanitise_content_for": sanitise_content_for}) return self.post("/v2/notifications/email", data=notification) diff --git a/tests/notifications_python_client/test_notifications_api_client.py b/tests/notifications_python_client/test_notifications_api_client.py index c1f8b7c..740fbcf 100644 --- a/tests/notifications_python_client/test_notifications_api_client.py +++ b/tests/notifications_python_client/test_notifications_api_client.py @@ -174,6 +174,25 @@ def test_create_email_notification_with_personalisation(notifications_client, rm } +def test_create_email_notification_with_sanitise_content_for(notifications_client, rmock): + endpoint = f"{TEST_HOST}/v2/notifications/email" + rmock.request("POST", endpoint, json={"status": "success"}, status_code=200) + + notifications_client.send_email_notification( + email_address="to@example.com", + template_id="456", + personalisation={"name": "chris", "link": "https://www.safe-link.gov.uk"}, + sanitise_content_for=["name"], + ) + + assert rmock.last_request.json() == { + "template_id": "456", + "email_address": "to@example.com", + "personalisation": {"name": "chris", "link": "https://www.safe-link.gov.uk"}, + "sanitise_content_for": ["name"], + } + + def test_create_email_notification_with_one_click_unsubscribe_url(notifications_client, rmock): endpoint = f"{TEST_HOST}/v2/notifications/email" rmock.request("POST", endpoint, json={"status": "success"}, status_code=200)