GHSA-4hxc-9384-m385 h3: SSE Event Injection via Unsanitized Carriage Return (`\r`) in EventStream Data and Comment Fields (Bypass of CVE Fix)
SummaryThe `EventStream` class in h3 fails to sanitize carriage return (`\r`) characters in `data` and `comment` fields. Per the SSE specification, `\r` is a valid line terminator, so browsers interpret injected `\r` as line breaks. This allows an attacker to inject arbitrary SSE events, spoof event types, and split a single `push()` call into multiple distinct browser-parsed events. This is an incomplete fix bypass of commit `7791538` which addressed `\n` injection but missed `\r`-only injection.DetailsThe prior fix in commit `7791538` added `_sanitizeSingleLine()` to strip `\n` and `\r` from `id` and `event` fields, and changed `data` formatting to split on `\n`. However, two code paths remain vulnerable:1. `data` field — `formatEventStreamMessage()` (`src/utils/internal/event-stream.ts:190-193`)`String.prototype.split("\n")` does **not** split on `\r`. A string like `"legit\revent: evil"` remains as a single "line" and is emitted as:Per the [SSE specification §9.2.6](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation), `\r` alone is a valid line terminator. The browser parses this as two separate lines:2. `comment` field — `formatEventStreamComment()` (`src/utils/internal/event-stream.ts:170-177`)The same `split("\n")` pattern means `\r` in comments is not handled. An input like `"x\rdata: injected"` produces:Which the browser parses as a comment line followed by actual data:Why `_sanitizeSingleLine` doesn't helpThe `_sanitizeSingleLine` function at line 198 correctly strips both `\r` and `\n`:But it is **only applied to `id` and `event` fields** (lines 182, 185), not to `data` or `comment`.PoCSetupCreate a minimal h3 application that reflects user input into an SSE stream:Attack 1: Event type injection via `\r` in data**Expected (safe) wire output:****Browser parses as:**The browser's `EventSource` fires a custom `evil` event instead of the default `message` event, potentially routing data to unintended handlers.Attack 2: Message boundary injection (event splitting)**Browser parses as two separate events:**Event 1: `data: first`Event 2: `data: injected`A single `push()` call produces two distinct events in the browser — the attacker controls the second event's content entirely.Attack 3: Comment escape to data injection**Browser parses as:**Impact**Event spoofing:** Attacker can inject arbitrary `event:` types, causing browsers to dispatch events to different `EventSource.addEventListener()` handlers than intended. In applications that use custom event types for control flow (e.g., `error`, `done`, `system`), this enables UI manipulation.**Message boundary injection:** A single `push()` call can be split into multiple browser-side events. This breaks application-level framing assumptions — e.g., a chat message could appear as two messages, or an injected "system" message could appear in an AI chat interface.**Comment-to-data escalation:** Data can be injected through what the application considers a harmless comment field via `pushComment()`.**Bypass of existing security control:** The prior fix (commit `7791538`) explicitly intended to prevent SSE injection, demonstrating the project considers this a security issue. The incomplete fix creates a false sense of security.Recommended FixBoth `formatEventStreamMessage` and `formatEventStreamComment` should split on `\r`, `\n`, and `\r\n` — matching the SSE spec's line terminator definition.This ensures all three SSE-spec line terminators (`\r\n`, `\r`, `\n`) are properly handled as line boundaries, preventing `\r` from being passed through to the browser where it would be interpreted as a line break.
CVSS v3.1 score: 5.3