Back to Blog
high SEVERITY6 min read

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

O
By Orbis AppSec
Published July 12, 2026Reviewed July 12, 2026

Answer Summary

CVE-2026-33128 is a Server-Sent Events (SSE) injection vulnerability in the h3 Node.js HTTP framework (CWE-93: CRLF Injection) where unsanitized newline characters in event stream fields allow attackers to inject arbitrary events into SSE connections. The fix is to upgrade h3 from version 1.15.5 to 1.15.6 (or 2.0.1-rc.15 for the v2 branch), which adds proper newline sanitization to all SSE field outputs.

Vulnerability at a Glance

cweCWE-93 (Improper Neutralization of CRLF Sequences)
fixUpgrade h3 from 1.15.5 to 1.15.6 which adds newline sanitization in SSE output
riskAttackers can inject arbitrary SSE events to manipulate client-side behavior
languageJavaScript/Node.js (h3 framework)
root causeh3 did not sanitize newline characters (\n, \r) in SSE event stream field values
vulnerabilityServer-Sent Events Injection via Unsanitized Newlines

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:

  1. An attacker submits content containing crafted newline sequences through normal application input (form fields, API calls, file uploads)
  2. The application stores this content and later streams it via SSE to connected clients
  3. The injected SSE events are delivered to all clients listening on that stream
  4. 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

  1. Always sanitize SSE field values: Even if your framework handles it, defense-in-depth means stripping \n and \r from any user-influenced data before passing it to SSE construction.

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

  3. Enable dependency scanning in CI: Tools like Trivy, Snyk, or GitHub Dependabot can catch known vulnerabilities in your dependency tree before they reach production.

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

  5. 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 createEventStream or eventStream.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-stream response without newline sanitization
  • Missing control: No sanitization or encoding of \n and \r characters 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.

References

Frequently Asked Questions

What is Server-Sent Events Injection?

SSE Injection occurs when an attacker can insert newline characters into event stream fields, breaking the SSE protocol framing and injecting arbitrary events that the client interprets as legitimate server messages.

How do you prevent SSE Injection in Node.js?

Sanitize all user-controlled data before including it in SSE fields by stripping or encoding newline characters (\n, \r, \r\n), and use frameworks that handle this sanitization automatically like h3 >= 1.15.6.

What CWE is SSE Injection?

SSE Injection falls under CWE-93 (Improper Neutralization of CRLF Sequences in HTTP Headers), as it exploits the same class of newline injection but in the context of the text/event-stream protocol.

Is output encoding enough to prevent SSE Injection?

Standard HTML output encoding is not sufficient — SSE requires specific newline sanitization because the protocol uses newlines as field delimiters. You must strip or replace \n and \r characters in all field values.

Can static analysis detect SSE Injection?

Yes, tools like Trivy can detect known vulnerable versions of libraries like h3 through dependency scanning, and SAST tools can flag patterns where user input flows into SSE event construction without newline sanitization.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #4125

Related Articles

high

How Octal IP Address Parsing Inconsistency Enables SSRF in Node.js and How to Fix It

A critical parsing inconsistency in the `ip-address` npm package (version 10.2.0) allowed attackers to bypass SSRF protections by exploiting how leading-zero octets are interpreted differently—decimal by the library versus octal by system resolvers. This vulnerability (CVE-2026-69192) was fixed by upgrading to version 10.3.1 using an npm override, ensuring consistent IP address validation across the application.

high

How Command Injection Happens in Node.js Child Process Calls and How to Fix It

A Node.js library was vulnerable to command injection through unsafe use of `execSync()` with shell string interpolation in the `index.js` file. By switching to `execFileSync()` with argument arrays, the fix eliminates the ability for attackers to inject shell metacharacters through file paths. This change demonstrates a critical security hardening pattern for any Node.js code that spawns child processes.

high

How NO_PROXY bypass via crafted URL happens in Node.js axios and how to fix it

A high-severity vulnerability (CVE-2026-42043) in the axios HTTP client library allowed attackers to bypass NO_PROXY environment variable restrictions using specially crafted URLs. This could route sensitive internal traffic through attacker-controlled proxy servers. The fix upgrades axios from 1.13.6 to 1.18.0, which includes a rewritten proxy resolution mechanism using `proxy-from-env` v2.1.0 and the `https-proxy-agent` package.

high

How Denial of Service via Infinite Loop happens in Node.js dependencies and how to fix it

A high-severity vulnerability in the nanoid package (CVE-2026-67213) allowed attackers to trigger infinite loops through the customAlphabet function, potentially causing complete denial of service. This fix upgrades nanoid from version 3.3.16 to 3.3.17 in the app_store dependency tree, eliminating the DoS risk through a simple version override.

high

How Denial of Service via Deeply Nested Field Names Happens in Node.js Multer and How to Fix It

A high-severity Denial of Service vulnerability (CVE-2026-5079) was discovered in the multer package, a popular Node.js middleware for handling multipart form data. Attackers could craft malicious requests with deeply nested field names to exhaust server resources. The fix upgrades multer from version 2.0.2 to 2.2.0, which implements proper limits on field name parsing depth.

critical

How SQL Injection happens in PHP PDO queries and how to fix it

A critical SQL injection vulnerability was discovered in the `getOfficialContests()` method of ContestRepository.php, where the `$site_id` parameter was directly interpolated into a SQL query string instead of using prepared statements. This vulnerability allowed attackers to inject arbitrary SQL commands and potentially access or manipulate the entire contest database. The fix replaced `pdo->query()` with `pdo->prepare()` and proper parameter binding.