Skip to content

Commit c7fb785

Browse files
authored
♻️ Validate Server Sent Event fields to avoid applications from sending broken data (#15588)
1 parent cb83b83 commit c7fb785

2 files changed

Lines changed: 27 additions & 5 deletions

File tree

fastapi/sse.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,20 @@ class EventSourceResponse(StreamingResponse):
3333
media_type = "text/event-stream"
3434

3535

36-
def _check_id_no_null(v: str | None) -> str | None:
36+
def _check_single_line(v: str | None, field_name: str) -> str | None:
37+
if v is not None and ("\r" in v or "\n" in v):
38+
raise ValueError(f"SSE '{field_name}' must be a single line")
39+
return v
40+
41+
42+
def _check_event_single_line(v: str | None) -> str | None:
43+
return _check_single_line(v, "event")
44+
45+
46+
def _check_id_valid(v: str | None) -> str | None:
3747
if v is not None and "\0" in v:
3848
raise ValueError("SSE 'id' must not contain null characters")
39-
return v
49+
return _check_single_line(v, "id")
4050

4151

4252
class ServerSentEvent(BaseModel):
@@ -86,24 +96,27 @@ class ServerSentEvent(BaseModel):
8696
] = None
8797
event: Annotated[
8898
str | None,
99+
AfterValidator(_check_event_single_line),
89100
Doc(
90101
"""
91102
Optional event type name.
92103
93104
Maps to `addEventListener(event, ...)` on the browser. When omitted,
94-
the browser dispatches on the generic `message` event.
105+
the browser dispatches on the generic `message` event. Must be a
106+
single line.
95107
"""
96108
),
97109
] = None
98110
id: Annotated[
99111
str | None,
100-
AfterValidator(_check_id_no_null),
112+
AfterValidator(_check_id_valid),
101113
Doc(
102114
"""
103115
Optional event ID.
104116
105117
The browser sends this value back as the `Last-Event-ID` header on
106-
automatic reconnection. **Must not contain null (`\\0`) characters.**
118+
automatic reconnection. **Must be a single line** and must not contain
119+
null (`\\0`) characters.
107120
"""
108121
),
109122
] = None

tests/test_sse.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,15 @@ def test_server_sent_event_null_id_rejected():
221221
ServerSentEvent(data="test", id="has\0null")
222222

223223

224+
@pytest.mark.parametrize("field_name", ["event", "id"])
225+
@pytest.mark.parametrize("value", ["first\nsecond", "first\rsecond", "first\r\nsecond"])
226+
def test_server_sent_event_single_line_fields_reject_newlines(
227+
field_name: str, value: str
228+
):
229+
with pytest.raises(ValueError, match=f"SSE '{field_name}' must be a single line"):
230+
ServerSentEvent(data="test", **{field_name: value})
231+
232+
224233
def test_server_sent_event_negative_retry_rejected():
225234
with pytest.raises(ValueError):
226235
ServerSentEvent(data="test", retry=-1)

0 commit comments

Comments
 (0)