Jan 16, 2026·6 min read·77 visits
H3, the underlying HTTP engine for Nuxt and Nitro, checked for the string "chunked" using a case-sensitive match. Attackers can send "ChuNked" to bypass body parsing on the backend while proxies still forward the body. This leaves leftover data on the TCP socket, causing subsequent requests to be hijacked.
A critical HTTP Request Smuggling vulnerability in the H3 framework allows attackers to desynchronize sockets by using mixed-case 'Transfer-Encoding' headers, leading to potential cache poisoning and request hijacking.
In the modern JavaScript ecosystem, everyone wants speed. We want lightweight, zero-dependency, blazingly fast frameworks. Enter H3, the minimal HTTP framework that powers the heavy hitters like Nuxt and Nitro. It is designed to be the bare metal engine of the server-side JS world.
But here is the thing about minimalism: when you strip away the "bloat," you often strip away the decades of scar tissue that older web servers have accumulated. That scar tissue exists for a reason. It protects against the weird, dusty corners of the HTTP specifications.
CVE-2026-23527 is a perfect example of what happens when modern code meets ancient protocols. By trying to be efficient with a simple string check, H3 accidentally opened the door to one of the most dangerous web attack classes: HTTP Request Smuggling. It turns out, the internet is not case-sensitive, even if JavaScript is.
To understand this bug, you need to look at RFC 9112 (or its grandfather, RFC 7230). The spec is crystal clear: the Transfer-Encoding header value is case-insensitive. A server must treat chunked, Chunked, and cHuNkEd exactly the same way.
Most industrial-grade proxies (like Nginx, HAProxy, or AWS ALB) follow the rules. They see Transfer-Encoding: ChuNked, nod their heads, and process the request body as a stream of chunks. They then forward that request to the backend application.
The H3 framework, however, failed to read the fine print. In its utility function readRawBody, it needed to decide if a request had a body to read. It checked the Content-Length. If that was missing, it looked at Transfer-Encoding. So far, so good. But the implementation specifically looked for the exact lowercase string "chunked".
This created a "TE.Skip" scenario. If an attacker sends Transfer-Encoding: ChuNked, the frontend proxy sees a body. H3 sees... nothing. H3 thinks the request is done, sends a response, and leaves the actual request body sitting unread in the TCP socket buffer. That unread data becomes the start of the next request on that connection.
Let's look at the vulnerable code in src/utils/body.ts. It's a classic case of "it works on my machine" logic.
// THE VULNERABLE CODE
if (
!Number.parseInt(event.node.req.headers["content-length"] || "") &&
!String(event.node.req.headers["transfer-encoding"] ?? "")
.split(",")
.map((e) => e.trim())
.filter(Boolean)
.includes("chunked") // <--- The fatal flaw
) {
return Promise.resolve(undefined);
}The developer assumed that includes("chunked") was sufficient. But "ChuNked" does not equal "chunked" in JavaScript. This check fails, the function returns undefined (no body), and the desynchronization begins.
Here is the fix introduced in version 1.15.5. They replaced the naive string splitting with a proper case-insensitive Regular Expression.
// THE FIX (Commit 618ccf4f37b8b6148bea7f36040471af45bfb097)
// ...
!/\bchunked\b/i.test( // <--- Regex with 'i' flag for case-insensitivity
String(event.node.req.headers["transfer-encoding"] ?? ""),
)
// ...This simple regex /\bchunked\b/i ensures that CHUnkED, Chunked, and chunked are all correctly identified, closing the desync window.
Exploiting this requires the H3 application to be sitting behind a reverse proxy that reuses backend connections (keep-alive). Here is the attack flow:
Transfer-Encoding: ChuNked. We define a chunk size, but then we hide a second HTTP request inside what looks like the body.POST /api/trigger HTTP/1.1
Host: vulnerable-app.com
Transfer-Encoding: ChuNked
0
GET /admin/delete-users HTTP/1.1
Host: vulnerable-app.com
X-Ignore: XThe Desync:
ChuNked. Parses the body. Sends the whole blob to H3.ChuNked. Says "I don't know her." Ignores the headers. Sees no Content-Length. Decides the request has no body. It handles POST /api/trigger immediately and sends back a 200 OK.The Poison: The H3 server has stopped reading socket data for the first request. But the bytes GET /admin/delete-users... are still sitting in the TCP buffer.
The Victim: The next request that comes in (maybe from a legitimate user, or the attacker's second request) will pick up where the socket left off. The H3 server reads the leftover bytes as the start of the new request. The legitimate user's request is effectively completely rewritten or appended as garbage.
In a void, this just causes socket errors. In reality, it causes chaos. Since H3 is often used with Nuxt (a meta-framework for Vue.js), these applications are frequently heavily cached.
Cache Poisoning: An attacker can smuggle a request that generates a specific response (e.g., a 404 or a malicious redirection). If the next request comes from a victim asking for index.js, and the server responds with the attacker's smuggled content, the intermediate cache might save that malicious content. Now every user requesting index.js gets the attacker's payload.
Authentication Bypass: If the smuggled request hits an admin endpoint, and the architecture relies on the frontend proxy for authentication validation (which is bypassed because the proxy thinks it is sending one request, not two), you can bypass access controls.
Queue Poisoning: If the application uses the request body to queue jobs, you can inject malicious jobs that the system validates incorrectly.
If you are running H3 (or Nuxt/Nitro) versions older than 1.15.5, you are vulnerable. The fix is straightforward:
npm install h3@latest or npx nuxi upgrade. Ensure your lockfiles are updated.Transfer-Encoding to lowercase before forwarding, or reject ambiguous requests entirely.Transfer-Encoding headers with mixed casing if your backend cannot handle them, though patching the backend is the only true fix.CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
h3 Unjs | < 1.15.5 | 1.15.5 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-444 (Inconsistent Interpretation of HTTP Requests) |
| CVSS | 8.9 (High) |
| Attack Vector | Network (HTTP Request Smuggling) |
| Exploit Status | PoC Available |
| Root Cause | Case-sensitive string comparison on Transfer-Encoding |
| Architecture | Node.js / Server-side JavaScript |
The application acts as an intermediary (e.g., proxy) or backend and interprets HTTP requests inconsistently with other devices in the chain, allowing request smuggling.
OpenList version 4.2.3 and prior is vulnerable to an authorization bypass and metadata leakage. When configured with the Bleve search engine backend, OpenList fails to perform separator-aware path matching when validating tenant containment. This allows authenticated users to access sibling directories sharing similar name prefixes. Furthermore, the search backend returns unfiltered global result counts, leaking existence verification data of unauthorized files via side-channel analysis.
An authorization bypass vulnerability in OpenList version 4.2.3 and below allows authenticated users to read arbitrary files outside of their designated base directories due to an insecure path prefix check using Go's standard strings.HasPrefix function.
A security policy bypass vulnerability exists in the AWS API MCP Server (awslabs-aws-api-mcp-server) from version 0.2.13 through 1.3.46. When the server fails to load the read-only operations index during startup (due to transient network failures, file permission issues, or other exceptions), it logs a warning but continues running in an insecure, degraded state. Under this condition, the security policy engine fails open, silently skipping all subsequent security checks and consent prompts for the lifetime of the process. This permits unauthorized mutating AWS CLI commands to execute via indirect prompt injection attacks.
An incomplete escaping vulnerability in the npm package 'shescape' allows unauthenticated users to trigger dynamic shell expansions, absolute path disclosure, and command block break-outs on Unix and Windows systems.
A critical command injection vulnerability in the shescape npm library affects Windows systems when running shell commands using cmd.exe. The escaping function fails to neutralize parentheses, allowing attackers to close shell blocks and execute arbitrary commands.
An escaping bypass in the npm package shescape on Unix platforms utilizing the Dash shell can result in unauthorized home directory path disclosure. The vulnerability occurs when user input containing specific sequences is passed inside shell variable assignment contexts. Additionally, the patch addresses secondary security gaps related to Zsh extended globbing and Windows CMD command block breakouts.