fix(security): escape user input in validation error HTML
format_validation_errors interpolated Pydantic error messages directly into HTML. Some messages echo user input (e.g. "Invalid group name '<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) <noreply@anthropic.com>
This commit is contained in:
parent
437ad59658
commit
c52778326e
2 changed files with 16 additions and 1 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import html
|
||||||
import re
|
import re
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
@ -166,7 +167,9 @@ def format_validation_errors(exc: ValidationError) -> str:
|
||||||
display_msg = f"{label}: {raw}"
|
display_msg = f"{label}: {raw}"
|
||||||
else:
|
else:
|
||||||
display_msg = f"{label}: {msg}"
|
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:
|
if len(messages) == 1:
|
||||||
return f'<div role="alert">{messages[0]}</div>'
|
return f'<div role="alert">{messages[0]}</div>'
|
||||||
|
|
|
||||||
|
|
@ -292,3 +292,15 @@ class TestFormatValidationErrors:
|
||||||
result = format_validation_errors(exc)
|
result = format_validation_errors(exc)
|
||||||
assert "Picture URL must be a valid HTTP or HTTPS URL" in result
|
assert "Picture URL must be a valid HTTP or HTTPS URL" in result
|
||||||
assert "Value error" not 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 = "<img src=x onerror=alert(1)>"
|
||||||
|
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 "<img" not in result
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue