Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-56848 — Exploit PoC for CVE-2026-56848, a Node.js HTTP/2 heap-use-after-free that allows remote unauthenticated DoS. Includes raw-socket trigger, ASan build instructions, and Docker-based target. | Kitploit
Tools/GitHubGitHub/open-flaw/cve-2026-56848
Vulnerability AnalysisDynamic Code Analysis (DAST)ExploitationWeb SecurityNetwork Security
GitHubopen-flaw/cve-2026-56848

CVE-2026-56848

Exploit PoC for CVE-2026-56848, a Node.js HTTP/2 heap-use-after-free that allows remote unauthenticated DoS. Includes raw-socket trigger, ASan build instructions, and Docker-based target.

View Repository
411 month agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-56848

NVD Description

A flaw in Node.js HTTP/2 handling allows nghttp2_session_mem_send() to be called re-entrantly while nghttp2_session_mem_recv() is executing, resulting in a heap-use-after-free.

This vulnerability affects Node.js 26.x, 24.x, and 22.x.

(Note: versions mentioned in the description apply only to the upstream nodejs package and not the nodejs package as distributed by Alpine.

Release lineVulnerableFixed
22.x (LTS)≤ 22.23.122.23.2
24.x (LTS)≤ 24.18.024.18.1
26.x≤ 26.5.026.5.1
  • Severity: High (CVSS 7.5, AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H — remote, unauthenticated DoS via heap corruption)
  • Reported by: hahahkim (HackerOne #3833629)
  • Fixed by: Matteo Collina (mcollina)
  • Fix commit (v22): daa6d25e3dce — "http2: defer rst stream while in scope" (nodejs-private/node-private#921)
  • Disclosed: Node.js security releases, 2026-07-29

Root Cause

Http2Stream::SubmitRstStream() in src/node_http2.cc forces a purge of pending outbound data before queueing the RST_STREAM:

void Http2Stream::SubmitRstStream(const uint32_t code) {
  CHECK(!this->is_destroyed());
  code_ = code;

  // (NGHTTP2_CANCEL is deferred — fix for an older double-free)
  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) {   // ← RE-ENTRANT mem_send()
    session_->AddPendingRstStream(id_);
    return;
  }

  FlushRstStream();
}

SendPendingData() calls nghttp2_session_mem_send() (node_http2.cc:1970). Its only re-entrancy guard is is_sending(), which protects against send-during-send (a write already in flight) — not against send-during-receive. When SubmitRstStream() runs from inside an nghttp2_session_mem_recv() callback chain ("in scope"), the purge runs mem_send() re-entrantly.

The re-entrant mem_send() flushes frames whose send-side processing tears down streams (nghttp2_session_close_stream_on_goaway() → on_stream_close → Http2Stream::Destroy() → free of the C++ Http2Stream). The freed stream is still referenced by the in-flight receive operation: SubmitRstStream() itself continues executing on the freed this (its trailing FlushRstStream() reads is_destroyed()), and the outer mem_recv() keeps walking frame/header state for the closed stream → heap-use-after-free.

Trigger chain (all inside a single nghttp2_session_mem_recv() call)

  1. Attacker sends GOAWAY(lastStreamID=0, NO_ERROR) immediately followed by HEADERS frames for new streams (3, 5, 7, …) in one TCP segment.
  2. Server's mem_recv() processes GOAWAY → JS session.close() → session.closed = true and an outbound GOAWAY is submitted but not yet sent.
  3. nghttp2 only refuses new incoming streams once GOAWAY is actually sent (session_allow_incoming_new_stream() checks TERM_ON_SEND | SENT, not SUBMITTED), so HEADERS(3) is still accepted.
  4. JS onSessionHeaders() sees a new stream on a closed session and refuses it: handle.rstStream(NGHTTP2_REFUSED_STREAM) (lib/internal/http2/core.js).
  5. C++ SubmitRstStream(NGHTTP2_REFUSED_STREAM) runs in scope (inside mem_recv), REFUSED_STREAM ≠ CANCEL → falls through to SendPendingData() → re-entrant nghttp2_session_mem_send().
  6. The re-entrant send flushes the outbound GOAWAY; nghttp2's send-side GOAWAY processing closes incoming streams with id > 1 (session_close_stream_on_goaway(..., NGHTTP2_REFUSED_STREAM)), firing on_stream_close → Http2Stream::Destroy() frees the C++ stream object for stream 3.
  7. Execution unwinds back into SubmitRstStream() on the freed object (FlushRstStream()), and the outer mem_recv() resumes on corrupted session/stream state → UAF.

Evidence from NODE_DEBUG_NATIVE=http2 on a vulnerable server (one 86-byte read):

receiving 86 bytes, offset 0
complete frame received: type: 7          ← GOAWAY
submitting goaway                          ← GOAWAY submitted, NOT yet sent
beginning headers for stream 3             ← still accepted (only SUBMITTED)
handle headers frame for stream 3          ← JS: session.closed → refuse
sending rst_stream with code 7             ← SubmitRstStream(REFUSED_STREAM), in scope
sending pending data                       ← RE-ENTRANT mem_send()
stream 3 closed with code: 7               ← GOAWAY send closes stream 3
Removing stream: 3 / destroying stream     ← Http2Stream freed mid-recv

Behavioral signature

The wire-level output is identical on vulnerable and patched builds (both end up sending only the outbound GOAWAY — on vulnerable builds the RST is submitted against an already-closed stream, on patched builds nghttp2 drops the queued RSTs once the GOAWAY goes out first). The difference is internal, visible with NODE_DEBUG_NATIVE=http2:

  • Vulnerable: sending pending data appears between sending rst_stream with code 7 and stream 3 closed with code: 7 — the re-entrant mem_send() runs mid-receive and closes/destroys stream 3 while mem_recv() is still in flight (crash under ASan).
  • Patched: no sending pending data between them — the RST is merely queued; stream 3 closes only during the normal post-receive flush.

PoC

server.js    # minimal http2.createServer() target (no handler needed)
exploit.js   # raw-socket HTTP/2 client that drives the trigger

Quick run

# Terminal 1: the target (any vulnerable node: 22.23.1 / 24.18.0 / 26.5.0 or older in their lines)
node server.js 8000                       # NODE_BIN=/path/to/node for a specific binary

# Terminal 2: the attack — a crash shows up in terminal 1 (ASan report / segfault)
node exploit.js --port 8000 --iterations 200

./bin/node (the local ASan build used below) is not tracked in git — build it with the instructions under "Building an ASan-instrumented vulnerable Node.js", or use the Docker route.

The exploit reports per-connection status; SKIPPED (no handshake) after the first connection means the target already died from the attack.

Docker

The Dockerfile builds a vulnerable v22.23.1 target with ASan inside a container (no local toolchain needed — only the Docker daemon):

docker build -t cve-2026-56848 .
docker run --rm -p 8000:8000 --name cve-target cve-2026-56848

# from the host, in another terminal:
# you could reuse ./bin/node
node exploit.js --port 8000 --iterations 10

# inspect the crash (ASan report) and exit code:
docker logs cve-target
docker inspect cve-target --format '{{.State.ExitCode}}'   # 133 (ASan abort) = crashed

Tip: if you already have an ASan-instrumented node binary built elsewhere, skip the long compile and package it directly:

docker run --name cve-img -v /path/to/out/Release:/opt/node debian:bookworm-slim \
  bash -c 'apt-get update -qq && apt-get install -y -qq libstdc++6 libatomic1 \
    && cp /opt/node/node /usr/local/bin/node-asan && mkdir -p /app'
docker cp server.js cve-img:/app/server.js
docker commit --change 'WORKDIR /app' --change 'EXPOSE 8000' \
  --change 'ENV HOST=0.0.0.0' --change 'ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=1' \
  --change 'ENTRYPOINT ["/usr/local/bin/node-asan"]' --change 'CMD ["server.js", "8000"]' \
  cve-img cve-2026-56848:verified

Verified against the containerized target: the first attack connection produces ERROR: AddressSanitizer: heap-use-after-free ... ABORTING in docker logs and the container exits (133 on linux/arm64) — same UAF as the native ASan run.

Download Tool