CVE-2026-56848: Node.js HTTP/2 RST_STREAM Heap Use-After-Free
How a stream reset submitted inside an active nghttp2 receive callback re-enters the send path and frees the session state the receive loop is still reading, crashing the server process.
.png)
On July 29, 2026, the Node.js project disclosed CVE-2026-56848 in its July 2026 security release, a High-severity heap use-after-free in the Node.js HTTP/2 server, reported by hahahkim through the Node.js HackerOne program and fixed by Matteo Collina. The HackerOne CNA scores it 7.5 (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H). The bug (CWE-416) lives in Http2Stream::SubmitRstStream() in src/node_http2.cc: when Node submits a RST_STREAM for a refused stream while nghttp2 is still inside nghttp2_session_mem_recv(), it forces a write that calls nghttp2_session_mem_send() re-entrantly, closing streams and freeing nghttp2 state that the active receive operation is still using. Affected releases are Node.js 26.x through 26.5.0, 24.x through 24.18.0, and 22.x through 22.23.1, fixed in 26.5.1, 24.18.1, and 22.23.2 respectively. The upstream advisory does not assess end-of-life lines, and the pre-fix code path is present in the final 20.x and 18.x releases, which have no OSS fix available.
Affected and unsupported? See NES for Node.js.
What is CVE-2026-56848?
CVE-2026-56848 is a use-after-free (CWE-416) in the node:http2 implementation, specifically in the C++ binding layer that mediates between Node's Http2Session / Http2Stream objects and the bundled nghttp2 library.
nghttp2 is a state machine driven by two calls. nghttp2_session_mem_recv() consumes bytes off the wire and fires callbacks as it parses frames. nghttp2_session_mem_send() serializes queued frames back out. nghttp2 does not expect the send call to be made from inside a callback fired by the receive call, because sending can close streams and release session structures that the receive loop still holds pointers into.
Node's HTTP/2 layer normally guards against this with Http2Scope, a stack guard that defers writes until the current nghttp2 operation unwinds. Before this fix, one path escaped the guard. When Node refuses a stream, nghttp2 returns NGHTTP2_ERR_REFUSED_STREAM, Node translates it to NGHTTP2_REFUSED_STREAM, and Http2Stream::SubmitRstStream() runs. That function short-circuited only for NGHTTP2_CANCEL. A refused stream fell through to session_->SendPendingData(), which calls nghttp2_session_mem_send() immediately, in the middle of the receive that produced the refusal.
The result is a classic re-entrancy use-after-free. The send path tears down stream state, the receive path resumes and dereferences it.
Root cause: the refused-stream path through SubmitRstStream
The vulnerable logic sits in src/node_http2.cc. Pre-fix, SubmitRstStream() checked for exactly one in-scope condition:
if (session_->is_in_scope() && is_stream_cancel(code)) {
session_->AddPendingRstStream(id_);
return;
}
// If possible, force a purge of any currently pending data here to make sure
// it is sent before closing the stream.
if (session_->SendPendingData() != 0) {
session_->AddPendingRstStream(id_);
return;
}
is_stream_cancel() returns true only for NGHTTP2_CANCEL. Every other reset code, including NGHTTP2_REFUSED_STREAM, reached SendPendingData() even when session_->is_in_scope() was true, meaning nghttp2 was already executing on the stack below.
The upstream fix (commit ba6cb5c, "http2: defer rst stream while in scope") adds a second guard that routes refused streams to FlushRstStream(), which submits the RST_STREAM frame inside a fresh Http2Scope so the actual write is deferred to scope exit rather than forced mid-receive:
// If RST_STREAM is submitted while nghttp2 is processing callbacks for
// a refused stream, don't force purge pending data. Sending pending data
// here can re-enter nghttp2 and close streams that are still being used
// by the active receive operation.
if (session_->is_in_scope() && code == NGHTTP2_REFUSED_STREAM) {
FlushRstStream();
return;
}
The commit ships with a regression test, test/parallel/test-http2-rst-stream-reentrancy.js, which reproduces the condition by driving a raw socket rather than the HTTP/2 client API.
Severity and exploit conditions
The HackerOne CNA scored CVE-2026-56848 at 7.5, High. The NVD entry was in "Received" status and had not yet completed NVD's own analysis at the time of writing, so the CNA score below is authoritative. Note that the CNA published a CVSS 3.0 vector rather than 3.1.
Exploit prerequisites are minimal. The target must be running an HTTP/2 server built on node:http2 (http2.createServer() or http2.createSecureServer()), and the attacker must be able to reach the listener and send raw frames. Applications that never enable HTTP/2 are not exposed through this path.
There is one caveat on the score. The CNA rated impact as availability only, and the observed upstream outcome is a crash. Use-after-free is a memory-corruption bug class, so treat the availability rating as the confirmed floor rather than a guarantee that no other impact is reachable.
Exploitation status
There is no public proof-of-concept exploit for CVE-2026-56848 as of this writing, and no working payload has been published by the reporter, by the Node.js project, or by third parties. The originating HackerOne report remains private.
CVE-2026-56848 is not listed in the CISA Known Exploited Vulnerabilities catalog (catalog verified August 13, 2026). CISA's SSVC assessment recorded in the NVD entry lists exploitation status as none, automatable as yes, and technical impact as partial. No active exploitation has been reported.
The public regression test in the Node.js repository documents the triggering frame sequence, which meaningfully lowers the bar for anyone building a crasher: connection preface, SETTINGS, one open stream, then a client-initiated GOAWAY followed immediately by HEADERS frames for additional stream IDs. That is a short, deterministic sequence over a single connection, which is consistent with CISA's "automatable: yes" rating. Patch on that basis rather than in the absence of a public exploit.
What an attacker can do
A remote, unauthenticated attacker who can reach an affected HTTP/2 listener can:
- Crash the Node.js process outright by opening one TCP connection, completing the HTTP/2 handshake, opening a stream, then sending a client-initiated GOAWAY followed by HEADERS frames for new stream IDs. The new streams are refused, and the refusal path frees the session state still in use by the in-flight nghttp2_session_mem_recv() call.
- Terminate every other connection served by the same process, including unrelated tenants, long-lived streams, and in-flight requests. HTTP/2 multiplexes many logical requests onto one process, so a single crash has a wide blast radius.
- Repeat the sequence on reconnect. There is no rate limit, credential check, or request budget in the path, so a supervisor that restarts the process simply provides a new target. Sustained repetition holds the service down.
- Bypass application-layer defenses entirely. The trigger is framed at the HTTP/2 transport layer and never reaches a route handler, middleware, authentication check, or WAF rule that inspects request semantics.
For services that terminate HTTP/2 directly on Node, for gRPC-over-HTTP/2 backends, and for any Node process exposed to untrusted clients, this is a straightforward remote denial of service.
Who is affected?
Only applications that create an HTTP/2 server are exposed through this path. If your service is HTTP/1.1 only and never calls http2.createServer() or http2.createSecureServer(), this specific bug does not reach you, though the other ten CVEs in the same release may.
Node.js version support dates are published on the Node.js release schedule. Node.js 22 reaches end-of-life on April 30, 2027, Node.js 24 on April 30, 2028, and Node.js 26 on April 30, 2029.
What we validated on end-of-life lines
The upstream advisory lists 26.x, 24.x, and 22.x as affected. That list reflects the lines the Node.js project actively assesses, not the full set of lines carrying the code.
We read Http2Stream::SubmitRstStream() in the final releases of both end-of-life lines. In Node.js v20.20.2 (the last 20.x release, EOL April 30, 2026) and Node.js v18.20.8, the function contains the pre-fix logic: the is_stream_cancel() short-circuit is present, and neither the is_receiving() guard nor the is_in_scope() && code == NGHTTP2_REFUSED_STREAM guard exists. Refused streams reach SendPendingData() exactly as they did in the affected supported lines.
Those lines will not receive an upstream assessment or an OSS fix, because they are past end-of-life. If you are running Node.js 20.x or 18.x with HTTP/2 enabled, treat the exposure as real and unpatched rather than as absent from the advisory.
Mitigation guidance
Upgrading within a supported line is a patch-level change and carries no API break. Moving from an end-of-life line to a supported one is a different exercise, since it can surface native module rebuilds, OpenSSL behavior changes, and transitive dependency conflicts, which is the gap remediated end-of-life builds are meant to close.
Related CVEs
CVE-2026-56848 shipped as one of eleven CVEs in the July 29, 2026 Node.js security release. The siblings worth tracking alongside it:
- CVE-2026-58043: High-severity Permission Model flaw where radix-tree prefix boundary handling can over-grant filesystem access beyond the --allow-fs-read and --allow-fs-write allowlist. Same release, and the second High-severity issue in the batch alongside the two HTTP/2 bugs.
- CVE-2026-21717: Node.js HashDoS in V8, another remote availability issue reachable without authentication. We covered the mechanics in our analysis of the V8 HashDoS vulnerability.
- CVE-2026-56846: the other High-severity HTTP/2 bug in the same release, where retained header blocks evade maxSessionMemory accounting and drive remote memory exhaustion. It affects 24.x and 22.x, is fixed in the same releases, and shares a component with CVE-2026-56848. If you are patching for one, you are patching for both.
- CVE-2026-58044: HTTP parser header truncation past maxHeadersCount that can hide a Content-Length header from application visibility while it is still used internally, enabling request smuggling through forwarding proxies. Rated Low, but relevant to the same edge-facing deployments.
For the full batch, see our breakdown of the July 2026 Node.js security release, and for the end-of-life angle, Node.js 20 goes EOL: how to stay secure without a full migration.
Taking action
If you run Node.js 26.x, 24.x, or 22.x, upgrade to 26.5.1, 24.18.1, or 22.23.2. The Node.js advisory covers ten to eleven CVEs depending on the line, so this is a release worth taking whole rather than cherry-picking.
The harder decision belongs to teams on Node.js 20.x and 18.x. Those lines went end-of-life on April 30, 2026 and April 30, 2025. They are absent from the advisory not because the code is safe but because nobody upstream is looking, and we confirmed the pre-fix SubmitRstStream() logic is still sitting in their final releases. Every subsequent Node.js security release widens that gap, and an HTTP/2 listener on an unassessed runtime is a remote crash away from an outage with no vendor advisory to point at.
HeroDevs is an official Node.js partner through the OpenJS Foundation, and NES for Node.js delivers drop-in replacement builds that resolve vulnerabilities on end-of-life lines without forcing a runtime migration you are not ready for.
See NES for Node.js to confirm coverage for your version, or talk to our team about scoping an end-of-life Node.js estate.
Resources
View All Articles


