nodejs: Security fix for CVE-2026-48937

Pick patch from [1] also mentioned at Debian tracker in [2]

[1] https://github.com/nodejs/node/commit/a1a5bb968303229d4a3a7c9e389dc3ece6237b4c
[2] https://security-tracker.debian.org/tracker/CVE-2026-48937

Signed-off-by: Rohini Sangam <rsangam@mvista.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
Rohini Sangam authored and Anuj Mittal committed 2026-10-26 10:10:52 +05:30
1 parent 24c9df0563
commit 7356c16d4c
2 files changed
+350

No files matched your search

@@ -0,0 +1,349 @@
From a1a5bb968303229d4a3a7c9e389dc3ece6237b4c Mon Sep 17 00:00:00 2001
From: Tim Perry <pimterry@gmail.com>
Date: Wed, 22 Apr 2026 13:36:27 +0200
Subject: [PATCH] deps: fix integration issues with the latest nghttp2
This is a set of src & tests fixes for nghttp2 due to changes in
v1.67.0+ which require a selection of changes to how we handle
low-level protocol errors when using the latest versions of nghttp2,
changing both some src error handling and updating some tests to match.
Signed-off-by: Tim Perry <pimterry@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/62891
Backport-PR-URL: https://github.com/nodejs/node/pull/63195
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Refs: https://hackerone.com/reports/3658225
CVE: CVE-2026-48937
Upstream-Status: Backport [https://github.com/nodejs/node/commit/a1a5bb968303229d4a3a7c9e389dc3ece6237b4c]
Signed-off-by: Rohini Sangam <rsangam@mvista.com>
---
src/node_http2.cc | 31 +++++++
src/node_http2.h | 2 +
...t-http2-misbehaving-flow-control-paused.js | 33 ++++---
.../test-http2-misbehaving-flow-control.js | 33 ++++---
...tp2-options-max-headers-exceeds-nghttp2.js | 12 ++-
...http2-session-cleanup-on-nghttp2-goaway.js | 91 +++++++++++++++++++
6 files changed, 176 insertions(+), 26 deletions(-)
create mode 100644 test/parallel/test-http2-session-cleanup-on-nghttp2-goaway.js
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 4f44789b..5047e5c4 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -1246,6 +1246,24 @@ int Http2Session::OnFrameSent(nghttp2_session* handle,
void* user_data) {
Http2Session* session = static_cast<Http2Session*>(user_data);
session->statistics_.frame_sent += 1;
+
+ // If nghttp2 has internally terminated the session (e.g. due to a protocol
+ // error like oversized frames, padding errors, or HPACK compression
+ // failures), it calls nghttp2_session_terminate_session() directly which
+ // queues a GOAWAY but does not invoke any application-level callback.
+ // Detect that case here: a GOAWAY was sent but we never initiated it
+ // (no Close(), no session.close(), no session.goaway()).
+ //
+ // We set a flag here, and then throw the error at the end of
+ // SendPendingData, to wait until the GOAWAY is written before the session
+ // is torn down.
+ if (frame->hd.type == NGHTTP2_GOAWAY && !session->is_closing() &&
+ !session->is_destroyed() && !session->IsGracefulCloseInitiated() &&
+ !session->goaway_initiated_) {
+ Debug(session, "nghttp2 session terminated internally");
+ session->internal_goaway_sent_ = true;
+ }
+
return 0;
}
@@ -1976,6 +1994,18 @@ uint8_t Http2Session::SendPendingData() {
MaybeStopReading();
+ // If nghttp2 has internally torn down the session (detected in OnFrameSent)
+ // during the nghttp2_session_mem_send loop above, at this point we error:
+ if (internal_goaway_sent_) {
+ internal_goaway_sent_ = false;
+ if (!is_closing() && !is_destroyed()) {
+ Isolate* isolate = env()->isolate();
+ HandleScope scope(isolate);
+ Local<Value> arg = Integer::New(isolate, NGHTTP2_ERR_PROTO);
+ MakeCallback(env()->http2session_on_error_function(), 1, &arg);
+ }
+ }
+
return 0;
}
@@ -2901,6 +2931,7 @@ void Http2Session::Goaway(uint32_t code,
if (is_destroyed())
return;
+ goaway_initiated_ = true;
Http2Scope h2scope(this);
// the last proc stream id is the most recently created Http2Stream.
if (lastStreamID <= 0)
diff --git a/src/node_http2.h b/src/node_http2.h
index a60a7ba0..91046052 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -968,6 +968,8 @@ class Http2Session : public AsyncWrap,
// Flag to indicate that JavaScript has initiated a graceful closure
bool graceful_close_initiated_ = false;
+ bool goaway_initiated_ = false;
+ bool internal_goaway_sent_ = false;
};
struct Http2SessionPerformanceEntryTraits {
diff --git a/test/parallel/test-http2-misbehaving-flow-control-paused.js b/test/parallel/test-http2-misbehaving-flow-control-paused.js
index e54bb832..b9a0d5cb 100644
--- a/test/parallel/test-http2-misbehaving-flow-control-paused.js
+++ b/test/parallel/test-http2-misbehaving-flow-control-paused.js
@@ -47,36 +47,45 @@ const data = Buffer.from([
// Bad client! Bad!
//
// Fortunately, nghttp2 handles this situation for us by keeping track
-// of the flow control window and responding with a FLOW_CONTROL_ERROR
-// causing the stream to get shut down...
-//
-// At least, that's what is supposed to happen.
+// of the flow control window and sending GOAWAY to end the session.
let client;
const server = h2.createServer({ settings: { initialWindowSize: 36 } });
-server.on('stream', (stream) => {
+
+server.on('session', common.mustCall((session) => {
+ session.on('error', common.expectsError({
+ code: 'ERR_HTTP2_ERROR',
+ name: 'Error',
+ message: 'Protocol error'
+ }));
+}));
+
+server.on('stream', common.mustCall((stream) => {
// Set the high water mark to zero, since otherwise we still accept
// reads from the source stream (if we can consume them).
stream._readableState.highWaterMark = 0;
stream.pause();
stream.on('error', common.expectsError({
- code: 'ERR_HTTP2_STREAM_ERROR',
+ code: 'ERR_HTTP2_ERROR',
name: 'Error',
- message: 'Stream closed with error code NGHTTP2_FLOW_CONTROL_ERROR'
+ message: 'Protocol error'
}));
stream.on('close', common.mustCall(() => {
server.close();
client.destroy();
}));
stream.on('end', common.mustNotCall());
-});
+}));
-server.listen(0, () => {
- client = net.connect(server.address().port, () => {
+server.listen(0, common.mustCall(() => {
+ client = net.connect(server.address().port, common.mustCall(() => {
client.write(preamble);
client.write(data);
client.write(data);
client.write(data);
- });
-});
+
+ // TCP connection is closed by the server
+ client.on('close', common.mustCall());
+ }));
+}));
diff --git a/test/parallel/test-http2-misbehaving-flow-control.js b/test/parallel/test-http2-misbehaving-flow-control.js
index 6774be22..fb0f7a1e 100644
--- a/test/parallel/test-http2-misbehaving-flow-control.js
+++ b/test/parallel/test-http2-misbehaving-flow-control.js
@@ -58,18 +58,24 @@ const data = Buffer.from([
// Bad client! Bad!
//
// Fortunately, nghttp2 handles this situation for us by keeping track
-// of the flow control window and responding with a FLOW_CONTROL_ERROR
-// causing the stream to get shut down...
-//
-// At least, that's what is supposed to happen.
+// of the flow control window and sending GOAWAY to end the session.
let client;
const server = h2.createServer({ settings: { initialWindowSize: 18 } });
-server.on('stream', (stream) => {
+
+server.on('session', common.mustCall((session) => {
+ session.on('error', common.expectsError({
+ code: 'ERR_HTTP2_ERROR',
+ name: 'Error',
+ message: 'Protocol error'
+ }));
+}));
+
+server.on('stream', common.mustCall((stream) => {
stream.on('error', common.expectsError({
- code: 'ERR_HTTP2_STREAM_ERROR',
+ code: 'ERR_HTTP2_ERROR',
name: 'Error',
- message: 'Stream closed with error code NGHTTP2_FLOW_CONTROL_ERROR'
+ message: 'Protocol error'
}));
stream.on('close', common.mustCall(() => {
server.close(common.mustCall());
@@ -78,14 +84,17 @@ server.on('stream', (stream) => {
stream.resume();
stream.respond();
stream.end('ok');
-});
+}));
server.on('close', common.mustCall());
-server.listen(0, () => {
- client = net.connect(server.address().port, () => {
+server.listen(0, common.mustCall(() => {
+ client = net.connect(server.address().port, common.mustCall(() => {
client.write(preamble);
client.write(data);
client.write(data);
- });
-});
+
+ // TCP connection is closed by the server
+ client.on('close', common.mustCall());
+ }));
+}));
diff --git a/test/parallel/test-http2-options-max-headers-exceeds-nghttp2.js b/test/parallel/test-http2-options-max-headers-exceeds-nghttp2.js
index 7767dbbc..46f560f9 100644
--- a/test/parallel/test-http2-options-max-headers-exceeds-nghttp2.js
+++ b/test/parallel/test-http2-options-max-headers-exceeds-nghttp2.js
@@ -90,11 +90,19 @@ server.listen(0, common.mustCall(() => {
0,
common.mustCall(() => {
const client = h2.connect(`http://localhost:${server.address().port}`);
- client.on('error', common.mustNotCall());
+ // The server sends oversized headers that cause a compression error on
+ // the client side, so nghttp2 internally terminates the client session.
+ client.on('error', common.expectsError({
+ code: 'ERR_HTTP2_ERROR',
+ name: 'Error',
+ }));
const req = client.request();
req.on('response', common.mustNotCall());
- req.on('error', common.mustNotCall());
+ req.on('error', common.expectsError({
+ code: 'ERR_HTTP2_ERROR',
+ name: 'Error',
+ }));
req.end();
}),
);
diff --git a/test/parallel/test-http2-session-cleanup-on-nghttp2-goaway.js b/test/parallel/test-http2-session-cleanup-on-nghttp2-goaway.js
new file mode 100644
index 00000000..f3576f9f
--- /dev/null
+++ b/test/parallel/test-http2-session-cleanup-on-nghttp2-goaway.js
@@ -0,0 +1,91 @@
+'use strict';
+
+const common = require('../common');
+
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const http2 = require('http2');
+const net = require('net');
+
+// When nghttp2 internally sends a GOAWAY frame due to a protocol error, it
+// may call nghttp2_session_terminate_session() directly, bypassing the
+// on_invalid_frame_recv_callback entirely. This test ensures that even
+// in that scenario, we still correctly clean up the session & connection.
+//
+// This test reproduces this with a client who sends a frame header with
+// a length exceeding the default max_frame_size (16384). nghttp2 responds
+// with GOAWAY(FRAME_SIZE_ERROR) without notifying Node through any callback.
+
+const server = http2.createServer();
+
+server.on('session', common.mustCall((session) => {
+ session.on('error', common.expectsError({
+ code: 'ERR_HTTP2_ERROR',
+ name: 'Error',
+ message: 'Protocol error'
+ }));
+
+ session.on('close', common.mustCall(() => server.close()));
+}));
+
+server.listen(0, common.mustCall(() => {
+ const conn = net.connect({
+ port: server.address().port,
+ allowHalfOpen: true,
+ });
+
+ // HTTP/2 client connection preface.
+ conn.write('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n');
+
+ // Empty SETTINGS frame.
+ const settingsFrame = Buffer.alloc(9);
+ settingsFrame[3] = 0x04; // type: SETTINGS
+ conn.write(settingsFrame);
+
+ let inbuf = Buffer.alloc(0);
+ let state = 'settingsHeader';
+ let settingsFrameLength;
+
+ conn.on('data', (chunk) => {
+ inbuf = Buffer.concat([inbuf, chunk]);
+
+ switch (state) {
+ case 'settingsHeader':
+ if (inbuf.length < 9) return;
+ settingsFrameLength = inbuf.readUIntBE(0, 3);
+ inbuf = inbuf.slice(9);
+ state = 'readingSettings';
+ // Fallthrough
+ case 'readingSettings': {
+ if (inbuf.length < settingsFrameLength) return;
+ inbuf = inbuf.slice(settingsFrameLength);
+ state = 'done';
+
+ // ACK the server SETTINGS.
+ const ack = Buffer.alloc(9);
+ ack[3] = 0x04; // type: SETTINGS
+ ack[4] = 0x01; // flag: ACK
+ conn.write(ack);
+
+ // Send a HEADERS frame header claiming length 16385, which exceeds
+ // the default max_frame_size of 16384. nghttp2 checks the length
+ // before reading any payload, so no body is needed. This triggers
+ // nghttp2_session_terminate_session(FRAME_SIZE_ERROR) directly in
+ // nghttp2_session_mem_recv2 — bypassing on_invalid_frame_recv_callback.
+ const oversized = Buffer.alloc(9);
+ oversized.writeUIntBE(16385, 0, 3); // length: 16385 (one over max)
+ oversized[3] = 0x01; // type: HEADERS
+ oversized[4] = 0x04; // flags: END_HEADERS
+ oversized.writeUInt32BE(1, 5); // stream id: 1
+ conn.write(oversized);
+
+ // No need to write the data - the header alone triggers the check.
+ }
+ }
+ });
+
+ // The server must close the connection after sending GOAWAY:
+ conn.on('end', common.mustCall(() => conn.end()));
+ conn.on('close', common.mustCall());
+}));
--
2.44.4
@@ -28,6 +28,7 @@ SRC_URI = "http://nodejs.org/dist/v${PV}/node-v${PV}.tar.xz \
file://run-ptest \
file://CVE-2026-48617.patch \
file://CVE-2026-48619.patch \
file://CVE-2026-48937.patch \
"
SRC_URI:append:class-target = " \
file://0001-Using-native-binaries.patch \