From c52778326e98f251cc0a9d152b13daabf56c72b4 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Thu, 4 Jun 2026 10:23:32 +0200 Subject: [PATCH] fix(security): escape user input in validation error HTML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit format_validation_errors interpolated Pydantic error messages directly into HTML. Some messages echo user input (e.g. "Invalid group name ''"), so a crafted group name was reflected as raw HTML — a stored/reflected XSS. HTML-escape each formatted message before interpolation. Refs: porchlight-due Co-Authored-By: Claude Opus 4.8 (1M context) --- src/porchlight/validation.py | 5 ++++- tests/test_validation.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/porchlight/validation.py b/src/porchlight/validation.py index 5d3b48c..e675e0b 100644 --- a/src/porchlight/validation.py +++ b/src/porchlight/validation.py @@ -1,3 +1,4 @@ +import html import re from typing import Annotated from urllib.parse import urlparse @@ -166,7 +167,9 @@ def format_validation_errors(exc: ValidationError) -> str: display_msg = f"{label}: {raw}" else: display_msg = f"{label}: {msg}" - messages.append(display_msg) + # Escape: messages can echo user input (e.g. an invalid group name), + # and the result is interpolated into HTML below. + messages.append(html.escape(display_msg)) if len(messages) == 1: return f'
{messages[0]}
' diff --git a/tests/test_validation.py b/tests/test_validation.py index 2be1afc..93bf2ea 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -292,3 +292,15 @@ class TestFormatValidationErrors: result = format_validation_errors(exc) assert "Picture URL must be a valid HTTP or HTTPS URL" in result assert "Value error" not in result + + def test_escapes_user_input_in_error_message(self) -> None: + # A validation error message that echoes user input (e.g. an invalid + # group name) must not emit raw HTML — otherwise it is reflected XSS. + payload = "" + try: + GroupListInput(groups=payload) + except ValidationError as exc: + result = format_validation_errors(exc) + assert payload not in result + assert "<img src=x onerror=alert(1)>" in result + assert "