fix: reject newline characters in SSE event and id fields#15187
fix: reject newline characters in SSE event and id fields#15187subhashdasyam wants to merge 2 commits intofastapi:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR closes an SSE protocol injection vector by preventing CR/LF injection via the event and id fields in format_sse_event() / ServerSentEvent, aligning their handling with the already-safe data and comment behavior.
Changes:
- Add validation to reject CR/LF in
ServerSentEvent.eventand extendServerSentEvent.idvalidation to reject CR/LF. - Add defense-in-depth sanitization in
format_sse_event()by stripping CR/LF fromeventandidbefore encoding. - Add regression tests covering newline/CR rejection and low-level formatting sanitization.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
fastapi/sse.py |
Adds newline/CR validation for event/id and strips CR/LF in format_sse_event() to prevent SSE field-line injection. |
tests/test_sse.py |
Adds regression tests to ensure newline/CR are rejected at the model level and sanitized at the formatter level. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
`format_sse_event()` interpolated the `event` and `id` fields into SSE wire-format bytes without stripping or rejecting newline characters. A `\n` or `\r\n` in either field injects an additional SSE field-line into the stream — enabling event-type spoofing and fabricated data payloads in browser EventSource clients. The `data` and `comment` fields were already protected via `splitlines()`; the omission for `event` and `id` was an inconsistency, not a design decision. The identical bug class was fixed in Hono (GHSA-p6xx-57qc-3wxr). Changes: - Add `_check_event_no_newline()` validator and apply it to `ServerSentEvent.event` via `AfterValidator` - Extend `_check_id_no_null()` to also reject `\n` and `\r` - Strip `\r`/`\n` in `format_sse_event()` for `event` and `id` as defense-in-depth (low-level function, bypasses model validators) - Add 7 regression tests covering LF/CR rejection in both the model and the low-level formatting function Severity: Medium (client-side injection; server unaffected) CWE: CWE-116 — Improper Encoding for Output in a Different Plaintext Context OWASP: A03:2021 — Injection Prior art: Hono GHSA-p6xx-57qc-3wxr (same class, same fix pattern)
6b62388 to
8bfab52
Compare
|
Hi maintainers 👋 The Labels / check-labels CI check is failing because this PR is missing one of the required labels. This is a security fix (SSE protocol injection via unvalidated Could a maintainer please apply the |
|
Fix tracked in issue #15188. |
|
Thanks for the thorough fix and the regression tests! A few observations while reviewing: 1. Bare # e.g.
with pytest.raises(ValueError):
ServerSentEvent(event="chat\radmin_command")2. Inconsistency in 3. Overall the fix looks solid — the two-layer approach (validator + defense-in-depth |
…'id' `_check_event_no_newline` lacked a `\0` check that `_check_id_no_null_or_newline` has had since the original implementation. Similarly, `format_sse_event()` stripped `\0` from `id` but not from `event`. Changes: - Extend `_check_event_no_newline` to also reject `\0` characters - Strip `\0` in `format_sse_event()` for `event` (mirrors existing `id` behavior) - Add `test_server_sent_event_null_event_rejected` regression test `event` and `id` are now fully symmetric in both the model validator and the low-level wire formatter.
Thanks for looking closely at this. On point 2 -- The difference is intentional. These two layers have different jobs:
This pattern (raise at model level, sanitize at wire level) is the same approach used for On point 3 -- You are right, this was a real gap. Fixed in the follow-up commit:
|
I think this assumption is incorrect. In case of |
That distinction makes sense.
|
I think yes, but it's you PR and you should decide. |
Summary
format_sse_event()infastapi/sse.pyinterpolated theeventandidfieldsinto SSE wire-format bytes without stripping or rejecting newline characters.
A
\nor\r\nin either field injects an additional SSE field-line into thestream, enabling event-type spoofing and fabricated
data:payloads in browserEventSourceclients.The
dataandcommentfields in the same function are correctly protected viasplitlines(). The omission foreventandidis an inconsistency — not adesign decision.
Vulnerability class: SSE Protocol Injection (CRLF injection into wire format)
CWE: CWE-116 — Improper Encoding for Output in a Different Plaintext Context
OWASP: A03:2021 — Injection
Prior art: Hono GHSA-p6xx-57qc-3wxr — identical bug class, same fix pattern
Inconsistency before this fix
datasplitlines()loopcommentsplitlines()loopeventid\0checkretryinttype (no strings)What the injection looks like
A request like
GET /stream?type=chat%0Adata:%20{"cmd":"exec"}causesformat_sse_eventto produce:The browser's
EventSourcereceives achatevent with a fabricateddata:field prepended to the real payload. Frontends that process
event.dataline-by-line (common in streaming AI/MCP UIs) receive a corrupted payload.
Changes
fastapi/sse.py_check_event_no_newline()validator; apply toServerSentEvent.eventvia
AfterValidator(mirrors the existing_check_id_no_nullpattern)_check_id_no_null()to also reject\nand\r\r/\nfromeventandidinformat_sse_event()asdefense-in-depth (low-level function; callable without going through
the Pydantic model validators)
tests/test_sse.pyServerSentEvent.event,ServerSentEvent.id, and theformat_sse_event()low-level functionTest results
All 25 tests pass (
pytest tests/test_sse.py -v), including the 7 new ones.Severity
Low at framework level / Medium in affected applications. The vulnerability
only activates when a developer routes attacker-influenced input into the
event=or
id=fields — most correct uses pass static strings. The fix is a one-lineguard that prevents the framework from silently accepting malformed input.