How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it
Introduction
In a frontend application's dependency tree, we discovered a high-severity SSE injection vulnerability tracked as CVE-2026-33128 in the h3 HTTP framework. The vulnerable version — h3 1.15.5 — was pinned as a transitive dependency in frontend/pnpm-lock.yaml, meaning any Server-Sent Events endpoint served by the application's Nitro/Nuxt-based backend was susceptible to event stream manipulation.
The h3 framework is a minimal, high-performance HTTP framework for Node.js that powers Nitro, Nuxt, and many other tools in the UnJS ecosystem. Its SSE implementation failed to sanitize newline characters (\n, \r) in event stream field values, allowing an attacker who could influence the content of SSE fields to inject entirely new, fabricated events into the stream.
This matters because SSE is increasingly used for real-time features like notifications, live updates, and AI streaming responses — all contexts where injected events could trigger dangerous client-side behavior.
The Vulnerability Explained
How Server-Sent Events Work
The SSE protocol (text/event-stream) uses a simple text-based format where fields are separated by newlines:
event: message
data: Hello, world!
event: update
data: {"status": "complete"}
Each event is terminated by a double newline (\n\n). Individual fields (event:, data:, id:, retry:) are terminated by a single newline. This means newline characters have protocol-level significance.
The Flaw in h3 1.15.5
In h3 version 1.15.5, when constructing SSE event fields, the framework did not strip or encode newline characters from values passed into event stream construction. If application code passed user-influenced data into an SSE field — for example, setting the data field from a database record or API response that an attacker could influence — the attacker could embed newlines to break out of the current field and inject entirely new SSE events.
Consider a scenario where an application streams status updates:
import { createEventStream } from 'h3';
// Attacker controls statusMessage via earlier input
const statusMessage = userInput; // e.g., "done\n\nevent: admin\ndata: {\"role\":\"admin\"}"
eventStream.push({
event: 'status',
data: statusMessage
});
With h3 1.15.5, this would produce:
event: status
data: done
event: admin
data: {"role":"admin"}
The client's EventSource would interpret this as two separate events — the legitimate status event and a fabricated admin event that the attacker injected.
Attack Scenario for This Application
Given that this is a frontend application (likely Nuxt/Nitro-based given the h3 dependency), any SSE endpoint that includes user-influenced content in event fields is vulnerable. For example:
- An attacker submits content containing crafted newline sequences through normal application input (form fields, API calls, file uploads)
- The application stores this content and later streams it via SSE to connected clients
- The injected SSE events are delivered to all clients listening on that stream
- Client-side JavaScript handlers process the fabricated events as legitimate, potentially triggering unauthorized actions, displaying false information, or executing stored XSS payloads
The threat model notes this is a local CLI tool where "exploitation requires the attacker to control command-line arguments or input files." Even in this context, a malicious input file processed by the tool could inject events into any SSE stream the tool exposes during operation (e.g., a development server's hot-reload stream).
The Fix
The fix involves two changes across frontend/package.json and frontend/pnpm-lock.yaml:
1. Pinning h3 1.15.6 as a direct dependency (frontend/package.json)
// Before: h3 was only a transitive dependency (1.15.5)
// After: h3 is pinned as a direct dependency at the fixed version
{
"dependencies": {
"dotenv": "^17.2.3",
"embla-carousel-react": "^8.6.0",
"gsap": "^3.13.0",
+ "h3": "1.15.6",
"hast": "^1.0.0",
"katex": "^0.16.28",
"lucide-react": "^0.562.0"
}
}
By adding h3 as a direct dependency with an exact version pin (1.15.6, not ^1.15.6), the fix ensures that pnpm resolves to the patched version regardless of what other transitive dependencies might request.
2. Lock file resolution (frontend/pnpm-lock.yaml)
# Before
- h3@1.15.5:
- resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==}
# After
+ h3@1.15.6:
+ resolution: {integrity: sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ==}
The lock file now resolves to h3 1.15.6, which includes proper newline sanitization in all SSE field construction. The fix also removes the now-unnecessary cookie-es@1.2.2 resolution (replaced by 1.2.3) and updates ufo from 1.6.3 to 1.6.4 as part of the dependency tree reconciliation.
Why h3 1.15.6 Fixes the Issue
In h3 1.15.6, the SSE implementation sanitizes newline characters (\n, \r, \r\n) from all field values before writing them to the event stream. This means even if user-controlled data containing newlines reaches the SSE construction logic, the newlines are stripped or encoded, preventing protocol-level injection.
Prevention & Best Practices
-
Always sanitize SSE field values: Even if your framework handles it, defense-in-depth means stripping
\nand\rfrom any user-influenced data before passing it to SSE construction. -
Pin security-critical dependencies: Use exact version pins for security-critical packages rather than range specifiers. This prevents regression if a vulnerable version is re-introduced through dependency resolution.
-
Enable dependency scanning in CI: Tools like Trivy, Snyk, or GitHub Dependabot can catch known vulnerabilities in your dependency tree before they reach production.
-
Treat SSE like any injection context: Just as you sanitize HTML output to prevent XSS and SQL parameters to prevent SQLi, sanitize SSE field values to prevent event injection.
-
Monitor the UnJS security advisories: The h3/Nitro/Nuxt ecosystem is widely used — subscribe to security notifications for these packages.
Key Takeaways
- h3 1.15.5's SSE implementation treated newlines in field values as literal protocol delimiters, enabling injection of arbitrary events — upgrading to 1.15.6 adds the missing sanitization.
- Transitive dependencies are attack surface: h3 wasn't a direct dependency in this project, but its vulnerability was still exploitable through the framework stack.
- Pinning the fixed version as a direct dependency (
"h3": "1.15.6"in package.json) is the most reliable way to override a vulnerable transitive dependency in pnpm. - SSE injection is the event-stream equivalent of HTTP header injection (CRLF injection) — both exploit the same class of newline-as-delimiter semantics.
- Even local CLI tools with dev servers can be affected when they expose SSE endpoints (e.g., HMR/hot-reload streams).
How Orbis AppSec Detected This
- Source: User-influenced data flowing into h3's SSE event stream construction (any
createEventStreamoreventStream.push()call with unsanitized field values) - Sink: h3's internal SSE serialization in versions ≤ 1.15.5, which writes field values directly to the
text/event-streamresponse without newline sanitization - Missing control: No sanitization or encoding of
\nand\rcharacters in SSE field values before protocol serialization - CWE: CWE-93 (Improper Neutralization of CRLF Sequences in HTTP Headers)
- Fix: Upgraded h3 from 1.15.5 to 1.15.6, which adds newline sanitization to all SSE field outputs
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2026-33128 is a reminder that protocol-level injection vulnerabilities lurk in unexpected places. Server-Sent Events may seem like a simple, read-only streaming protocol, but the newline-delimited format creates an injection surface identical in principle to CRLF injection in HTTP headers. By upgrading h3 to 1.15.6, this project eliminated the risk of SSE event fabrication, protecting clients from receiving and acting on attacker-crafted events.
For any project using h3, Nitro, or Nuxt with SSE endpoints: check your h3 version and upgrade immediately. The fix is a drop-in replacement with no breaking changes.