mirror of
https://github.com/kennethreitz/responder.git
synced 2026-07-21 18:39:29 +00:00
4abd8bb8e9
## Summary
A runtime-hardening pass over the Server-Sent Events path (which the
earlier audit only skimmed) turned up a real injection gap in the SSE
frame encoder (`responder/models.py`).
`_sse_frame` interpolated the **single-line** fields — `event`, `id`,
`retry`, and the `comment` line — verbatim:
```python
lines.append(f"event: {event}") # before
```
SSE fields are terminated by a line break, so a `\r` or `\n` in any of
those values ends the field early and injects arbitrary SSE lines or
whole frames. That matters whenever those fields carry
caller/user-derived data — e.g. an event name, or an `id` echoed from a
client's `Last-Event-ID`. Separately, `data` only split on `\n`, so a
lone `\r` (a valid SSE line terminator per the spec) passed through and
could inject a new field.
## Fix
- Strip `\r`, `\n` (and `\x00`, which the spec forbids in `id`) from
`event`, `id`, `retry`, and comment values via a small
`_sse_single_line` helper.
- Normalize `data` on all line terminators (`\r\n`, `\r`, `\n`) before
splitting into `data:` lines, so multi-line data is encoded correctly
and no lone `\r` survives.
Legitimate multi-line `data` still works exactly as before.
## Tests
New `tests/test_sse_injection.py` (6 tests): newline stripped from
`event`/`id`/`retry`, NUL stripped from `id`, `data` split on all
terminators, comment scrubbed, and an end-to-end route asserting an
injected `data:` line can't appear. Existing `tests/test_sse.py` still
passes.
Full suite: **755 passed**, 1 skipped (php not installed); ruff clean;
mypy clean.
## Scope note
I checked the other runtime paths in the same neighborhood and found
them already solid, so this PR is deliberately narrow rather than a
grab-bag: request-body size limits are enforced while streaming (both
declared `Content-Length` and accumulated bytes, 413 on exceed);
streaming/response disconnect is delegated to Starlette's
`StreamingResponse`; SSE already sets `Cache-Control: no-cache` /
`X-Accel-Buffering: no` / keep-alive and supports a heartbeat;
open-redirects are guarded by `allow_external`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>