From c7fb7851b3389f24c51701d705458989be53ccbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 23 May 2026 19:23:05 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Validate=20Server=20Sent?= =?UTF-8?q?=20Event=20fields=20to=20avoid=20applications=20from=20sending?= =?UTF-8?q?=20broken=20data=20(#15588)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/sse.py | 23 ++++++++++++++++++----- tests/test_sse.py | 9 +++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/fastapi/sse.py b/fastapi/sse.py index 901d824964..1e2bd86171 100644 --- a/fastapi/sse.py +++ b/fastapi/sse.py @@ -33,10 +33,20 @@ class EventSourceResponse(StreamingResponse): media_type = "text/event-stream" -def _check_id_no_null(v: str | None) -> str | None: +def _check_single_line(v: str | None, field_name: str) -> str | None: + if v is not None and ("\r" in v or "\n" in v): + raise ValueError(f"SSE '{field_name}' must be a single line") + return v + + +def _check_event_single_line(v: str | None) -> str | None: + return _check_single_line(v, "event") + + +def _check_id_valid(v: str | None) -> str | None: if v is not None and "\0" in v: raise ValueError("SSE 'id' must not contain null characters") - return v + return _check_single_line(v, "id") class ServerSentEvent(BaseModel): @@ -86,24 +96,27 @@ class ServerSentEvent(BaseModel): ] = None event: Annotated[ str | None, + AfterValidator(_check_event_single_line), Doc( """ Optional event type name. Maps to `addEventListener(event, ...)` on the browser. When omitted, - the browser dispatches on the generic `message` event. + the browser dispatches on the generic `message` event. Must be a + single line. """ ), ] = None id: Annotated[ str | None, - AfterValidator(_check_id_no_null), + AfterValidator(_check_id_valid), Doc( """ Optional event ID. The browser sends this value back as the `Last-Event-ID` header on - automatic reconnection. **Must not contain null (`\\0`) characters.** + automatic reconnection. **Must be a single line** and must not contain + null (`\\0`) characters. """ ), ] = None diff --git a/tests/test_sse.py b/tests/test_sse.py index 6dfec61838..86a67f8f9f 100644 --- a/tests/test_sse.py +++ b/tests/test_sse.py @@ -221,6 +221,15 @@ def test_server_sent_event_null_id_rejected(): ServerSentEvent(data="test", id="has\0null") +@pytest.mark.parametrize("field_name", ["event", "id"]) +@pytest.mark.parametrize("value", ["first\nsecond", "first\rsecond", "first\r\nsecond"]) +def test_server_sent_event_single_line_fields_reject_newlines( + field_name: str, value: str +): + with pytest.raises(ValueError, match=f"SSE '{field_name}' must be a single line"): + ServerSentEvent(data="test", **{field_name: value}) + + def test_server_sent_event_negative_retry_rejected(): with pytest.raises(ValueError): ServerSentEvent(data="test", retry=-1)