Skip to content

Commit 309e090

Browse files
[AutoPR- Security] Patch nodejs for CVE-2026-21716, CVE-2026-21715, CVE-2026-21714, CVE-2026-21713, CVE-2026-21710 [HIGH] (#16411)
Co-authored-by: jslobodzian <joslobo@microsoft.com>
1 parent 31c1d3d commit 309e090

6 files changed

Lines changed: 529 additions & 1 deletion

File tree

SPECS/nodejs/CVE-2026-21710.patch

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
From 286a9c4e5b1b7bb7373195992a0f570c3eab8aaa Mon Sep 17 00:00:00 2001
2+
From: Matteo Collina <hello@matteocollina.com>
3+
Date: Thu, 19 Feb 2026 15:49:43 +0100
4+
Subject: [PATCH] http: use null prototype for headersDistinct/trailersDistinct
5+
6+
Use { __proto__: null } instead of {} when initializing the
7+
headersDistinct and trailersDistinct destination objects.
8+
9+
A plain {} inherits from Object.prototype, so when a __proto__
10+
header is received, dest["__proto__"] resolves to Object.prototype
11+
(truthy), causing _addHeaderLineDistinct to call .push() on it,
12+
which throws an uncaught TypeError and crashes the process.
13+
14+
Ref: https://hackerone.com/reports/3560402
15+
PR-URL: https://github.com/nodejs-private/node-private/pull/821
16+
Refs: https://hackerone.com/reports/3560402
17+
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
18+
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
19+
CVE-ID: CVE-2026-21710
20+
Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
21+
Upstream-reference: https://github.com/nodejs/node/commit/00ad47a28eb2e3dc0ff5610d58c53341acf3cf8d.patch
22+
---
23+
lib/_http_incoming.js | 4 +--
24+
.../test-http-headers-distinct-proto.js | 36 +++++++++++++++++++
25+
test/parallel/test-http-multiple-headers.js | 16 ++++-----
26+
3 files changed, 46 insertions(+), 10 deletions(-)
27+
create mode 100644 test/parallel/test-http-headers-distinct-proto.js
28+
29+
diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js
30+
index e45ae819..77433e55 100644
31+
--- a/lib/_http_incoming.js
32+
+++ b/lib/_http_incoming.js
33+
@@ -131,7 +131,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'headersDistinct', {
34+
__proto__: null,
35+
get: function() {
36+
if (!this[kHeadersDistinct]) {
37+
- this[kHeadersDistinct] = {};
38+
+ this[kHeadersDistinct] = { __proto__: null };
39+
40+
const src = this.rawHeaders;
41+
const dst = this[kHeadersDistinct];
42+
@@ -171,7 +171,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'trailersDistinct', {
43+
__proto__: null,
44+
get: function() {
45+
if (!this[kTrailersDistinct]) {
46+
- this[kTrailersDistinct] = {};
47+
+ this[kTrailersDistinct] = { __proto__: null };
48+
49+
const src = this.rawTrailers;
50+
const dst = this[kTrailersDistinct];
51+
diff --git a/test/parallel/test-http-headers-distinct-proto.js b/test/parallel/test-http-headers-distinct-proto.js
52+
new file mode 100644
53+
index 00000000..bd4cb82b
54+
--- /dev/null
55+
+++ b/test/parallel/test-http-headers-distinct-proto.js
56+
@@ -0,0 +1,36 @@
57+
+'use strict';
58+
+
59+
+const common = require('../common');
60+
+const assert = require('assert');
61+
+const http = require('http');
62+
+const net = require('net');
63+
+
64+
+// Regression test: sending a __proto__ header must not crash the server
65+
+// when accessing req.headersDistinct or req.trailersDistinct.
66+
+
67+
+const server = http.createServer(common.mustCall((req, res) => {
68+
+ const headers = req.headersDistinct;
69+
+ assert.strictEqual(Object.getPrototypeOf(headers), null);
70+
+ assert.deepStrictEqual(Object.getOwnPropertyDescriptor(headers, '__proto__').value, ['test']);
71+
+ res.end();
72+
+}));
73+
+
74+
+server.listen(0, common.mustCall(() => {
75+
+ const port = server.address().port;
76+
+
77+
+ const client = net.connect(port, common.mustCall(() => {
78+
+ client.write(
79+
+ 'GET / HTTP/1.1\r\n' +
80+
+ 'Host: localhost\r\n' +
81+
+ '__proto__: test\r\n' +
82+
+ 'Connection: close\r\n' +
83+
+ '\r\n',
84+
+ );
85+
+ }));
86+
+
87+
+ client.on('end', common.mustCall(() => {
88+
+ server.close();
89+
+ }));
90+
+
91+
+ client.resume();
92+
+}));
93+
diff --git a/test/parallel/test-http-multiple-headers.js b/test/parallel/test-http-multiple-headers.js
94+
index 8f52f817..d0fea85c 100644
95+
--- a/test/parallel/test-http-multiple-headers.js
96+
+++ b/test/parallel/test-http-multiple-headers.js
97+
@@ -27,13 +27,13 @@ const server = createServer(
98+
host,
99+
'transfer-encoding': 'chunked'
100+
});
101+
- assert.deepStrictEqual(req.headersDistinct, {
102+
+ assert.deepStrictEqual(req.headersDistinct, Object.assign({ __proto__: null }, {
103+
'connection': ['close'],
104+
'x-req-a': ['eee', 'fff', 'ggg', 'hhh'],
105+
'x-req-b': ['iii; jjj; kkk; lll'],
106+
'host': [host],
107+
- 'transfer-encoding': ['chunked']
108+
- });
109+
+ 'transfer-encoding': ['chunked'],
110+
+ }));
111+
112+
req.on('end', function() {
113+
assert.deepStrictEqual(req.rawTrailers, [
114+
@@ -46,7 +46,7 @@ const server = createServer(
115+
);
116+
assert.deepStrictEqual(
117+
req.trailersDistinct,
118+
- { 'x-req-x': ['xxx', 'yyy'], 'x-req-y': ['zzz; www'] }
119+
+ Object.assign({ __proto__: null }, { 'x-req-x': ['xxx', 'yyy'], 'x-req-y': ['zzz; www'] })
120+
);
121+
122+
res.setHeader('X-Res-a', 'AAA');
123+
@@ -129,14 +129,14 @@ server.listen(0, common.mustCall(() => {
124+
'x-res-d': 'JJJ; KKK; LLL',
125+
'transfer-encoding': 'chunked'
126+
});
127+
- assert.deepStrictEqual(res.headersDistinct, {
128+
+ assert.deepStrictEqual(res.headersDistinct, Object.assign({ __proto__: null }, {
129+
'x-res-a': [ 'AAA', 'BBB', 'CCC' ],
130+
'x-res-b': [ 'DDD; EEE; FFF; GGG' ],
131+
'connection': [ 'close' ],
132+
'x-res-c': [ 'HHH', 'III' ],
133+
'x-res-d': [ 'JJJ; KKK; LLL' ],
134+
- 'transfer-encoding': [ 'chunked' ]
135+
- });
136+
+ 'transfer-encoding': [ 'chunked' ],
137+
+ }));
138+
139+
res.on('end', function() {
140+
assert.deepStrictEqual(res.rawTrailers, [
141+
@@ -150,7 +150,7 @@ server.listen(0, common.mustCall(() => {
142+
);
143+
assert.deepStrictEqual(
144+
res.trailersDistinct,
145+
- { 'x-res-x': ['XXX', 'YYY'], 'x-res-y': ['ZZZ; WWW'] }
146+
+ Object.assign({ __proto__: null }, { 'x-res-x': ['XXX', 'YYY'], 'x-res-y': ['ZZZ; WWW'] })
147+
);
148+
server.close();
149+
});
150+
--
151+
2.45.4
152+

SPECS/nodejs/CVE-2026-21713.patch

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
From a3f686cc211731ef93e35798357debcc8a6dc752 Mon Sep 17 00:00:00 2001
2+
From: Filip Skokan <panva.ip@gmail.com>
3+
Date: Fri, 20 Feb 2026 12:32:14 +0100
4+
Subject: [PATCH] crypto: use timing-safe comparison in Web Cryptography HMAC
5+
6+
Use `CRYPTO_memcmp` instead of `memcmp` in `HMAC`
7+
Web Cryptography algorithm implementations.
8+
9+
Ref: https://hackerone.com/reports/3533945
10+
PR-URL: https://github.com/nodejs-private/node-private/pull/831
11+
Refs: https://hackerone.com/reports/3533945
12+
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
13+
CVE-ID: CVE-2026-21713
14+
Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
15+
Upstream-reference: https://github.com/nodejs/node/commit/cfb51fa9ce1da2a8c810ec35bcc7c000f8c94faf.patch
16+
---
17+
src/crypto/crypto_hmac.cc | 3 ++-
18+
1 file changed, 2 insertions(+), 1 deletion(-)
19+
20+
diff --git a/src/crypto/crypto_hmac.cc b/src/crypto/crypto_hmac.cc
21+
index 5d81a60a..c09aa70c 100644
22+
--- a/src/crypto/crypto_hmac.cc
23+
+++ b/src/crypto/crypto_hmac.cc
24+
@@ -268,7 +268,8 @@ Maybe<bool> HmacTraits::EncodeOutput(
25+
*result = Boolean::New(
26+
env->isolate(),
27+
out->size() > 0 && out->size() == params.signature.size() &&
28+
- memcmp(out->data(), params.signature.data(), out->size()) == 0);
29+
+ CRYPTO_memcmp(
30+
+ out->data(), params.signature.data(), out->size()) == 0);
31+
break;
32+
default:
33+
UNREACHABLE();
34+
--
35+
2.45.4
36+

SPECS/nodejs/CVE-2026-21714.patch

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
From 2b5f0f547445e892ef458e2819674f0094fc209e Mon Sep 17 00:00:00 2001
2+
From: RafaelGSS <rafael.nunu@hotmail.com>
3+
Date: Wed, 11 Mar 2026 11:22:23 -0300
4+
Subject: [PATCH] src: handle NGHTTP2_ERR_FLOW_CONTROL error code
5+
6+
Refs: https://hackerone.com/reports/3531737
7+
PR-URL: https://github.com/nodejs-private/node-private/pull/832
8+
CVE-ID: CVE-2026-21714
9+
Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
10+
Upstream-reference: https://github.com/nodejs/node/commit/a0c73425da4c95fbcf6c13b7fe8921301290b8e6.patch
11+
---
12+
src/node_http2.cc | 6 ++
13+
.../test-http2-window-update-overflow.js | 84 +++++++++++++++++++
14+
2 files changed, 90 insertions(+)
15+
create mode 100644 test/parallel/test-http2-window-update-overflow.js
16+
17+
diff --git a/src/node_http2.cc b/src/node_http2.cc
18+
index 28dfe532..ddf17380 100644
19+
--- a/src/node_http2.cc
20+
+++ b/src/node_http2.cc
21+
@@ -1114,8 +1114,14 @@ int Http2Session::OnInvalidFrame(nghttp2_session* handle,
22+
// The GOAWAY frame includes an error code that indicates the type of error"
23+
// The GOAWAY frame is already sent by nghttp2. We emit the error
24+
// to liberate the Http2Session to destroy.
25+
+ //
26+
+ // ERR_FLOW_CONTROL: A WINDOW_UPDATE on stream 0 pushed the connection-level
27+
+ // flow control window past 2^31-1. nghttp2 sends GOAWAY internally but
28+
+ // without propagating this error the Http2Session would never be destroyed,
29+
+ // causing a memory leak.
30+
if (nghttp2_is_fatal(lib_error_code) ||
31+
lib_error_code == NGHTTP2_ERR_STREAM_CLOSED ||
32+
+ lib_error_code == NGHTTP2_ERR_FLOW_CONTROL ||
33+
lib_error_code == NGHTTP2_ERR_PROTO) {
34+
Environment* env = session->env();
35+
Isolate* isolate = env->isolate();
36+
diff --git a/test/parallel/test-http2-window-update-overflow.js b/test/parallel/test-http2-window-update-overflow.js
37+
new file mode 100644
38+
index 00000000..41488af9
39+
--- /dev/null
40+
+++ b/test/parallel/test-http2-window-update-overflow.js
41+
@@ -0,0 +1,84 @@
42+
+'use strict';
43+
+
44+
+const common = require('../common');
45+
+
46+
+if (!common.hasCrypto)
47+
+ common.skip('missing crypto');
48+
+
49+
+const http2 = require('http2');
50+
+const net = require('net');
51+
+
52+
+// Regression test: a connection-level WINDOW_UPDATE that causes the flow
53+
+// control window to exceed 2^31-1 must destroy the Http2Session (not leak it).
54+
+//
55+
+// nghttp2 responds with GOAWAY(FLOW_CONTROL_ERROR) internally but previously
56+
+// Node's OnInvalidFrame callback only propagated errors for
57+
+// NGHTTP2_ERR_STREAM_CLOSED and NGHTTP2_ERR_PROTO. The missing
58+
+// NGHTTP2_ERR_FLOW_CONTROL case left the session unreachable after the GOAWAY,
59+
+// causing a memory leak.
60+
+
61+
+const server = http2.createServer();
62+
+
63+
+server.on('session', common.mustCall((session) => {
64+
+ session.on('error', common.mustCall());
65+
+ session.on('close', common.mustCall(() => server.close()));
66+
+}));
67+
+
68+
+server.listen(0, common.mustCall(() => {
69+
+ const conn = net.connect({
70+
+ port: server.address().port,
71+
+ allowHalfOpen: true,
72+
+ });
73+
+
74+
+ // HTTP/2 client connection preface.
75+
+ conn.write('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n');
76+
+
77+
+ // Empty SETTINGS frame (9-byte header, 0-byte payload).
78+
+ const settingsFrame = Buffer.alloc(9);
79+
+ settingsFrame[3] = 0x04; // type: SETTINGS
80+
+ conn.write(settingsFrame);
81+
+
82+
+ let inbuf = Buffer.alloc(0);
83+
+ let state = 'settingsHeader';
84+
+ let settingsFrameLength;
85+
+
86+
+ conn.on('data', (chunk) => {
87+
+ inbuf = Buffer.concat([inbuf, chunk]);
88+
+
89+
+ switch (state) {
90+
+ case 'settingsHeader':
91+
+ if (inbuf.length < 9) return;
92+
+ settingsFrameLength = inbuf.readUIntBE(0, 3);
93+
+ inbuf = inbuf.slice(9);
94+
+ state = 'readingSettings';
95+
+ // Fallthrough
96+
+ case 'readingSettings': {
97+
+ if (inbuf.length < settingsFrameLength) return;
98+
+ inbuf = inbuf.slice(settingsFrameLength);
99+
+ state = 'done';
100+
+
101+
+ // ACK the server SETTINGS.
102+
+ const ack = Buffer.alloc(9);
103+
+ ack[3] = 0x04; // type: SETTINGS
104+
+ ack[4] = 0x01; // flag: ACK
105+
+ conn.write(ack);
106+
+
107+
+ // WINDOW_UPDATE on stream 0 (connection level) with increment 2^31-1.
108+
+ // Default connection window is 65535, so the new total would be
109+
+ // 65535 + 2147483647 = 2147549182 > 2^31-1, triggering
110+
+ // NGHTTP2_ERR_FLOW_CONTROL inside nghttp2.
111+
+ const windowUpdate = Buffer.alloc(13);
112+
+ windowUpdate.writeUIntBE(4, 0, 3); // length = 4
113+
+ windowUpdate[3] = 0x08; // type: WINDOW_UPDATE
114+
+ windowUpdate[4] = 0x00; // flags: none
115+
+ windowUpdate.writeUIntBE(0, 5, 4); // stream id: 0
116+
+ windowUpdate.writeUIntBE(0x7FFFFFFF, 9, 4); // increment: 2^31-1
117+
+ conn.write(windowUpdate);
118+
+ }
119+
+ }
120+
+ });
121+
+
122+
+ // The server must close the connection after sending GOAWAY.
123+
+ conn.on('end', common.mustCall(() => conn.end()));
124+
+ conn.on('close', common.mustCall());
125+
+}));
126+
--
127+
2.45.4
128+

SPECS/nodejs/CVE-2026-21715.patch

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
From eda20a5f65a85abd2dada7303c58e2775abe1db7 Mon Sep 17 00:00:00 2001
2+
From: RafaelGSS <rafael.nunu@hotmail.com>
3+
Date: Mon, 5 Jan 2026 18:18:39 -0300
4+
Subject: [PATCH] permission: add permission check to realpath.native
5+
6+
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
7+
PR-URL: https://github.com/nodejs-private/node-private/pull/838
8+
CVE-ID: CVE-2026-21715
9+
Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
10+
Upstream-reference: https://github.com/nodejs/node/commit/00830712bc623ba04b08856462a56b79e29f5cc3.patch
11+
---
12+
src/node_file.cc | 8 ++++++++
13+
test/fixtures/permission/fs-read.js | 13 +++++++++++++
14+
2 files changed, 21 insertions(+)
15+
16+
diff --git a/src/node_file.cc b/src/node_file.cc
17+
index ba69879b..32e85203 100644
18+
--- a/src/node_file.cc
19+
+++ b/src/node_file.cc
20+
@@ -1909,11 +1909,19 @@ static void RealPath(const FunctionCallbackInfo<Value>& args) {
21+
22+
if (argc > 2) { // realpath(path, encoding, req)
23+
FSReqBase* req_wrap_async = GetReqWrap(args, 2);
24+
+ CHECK_NOT_NULL(req_wrap_async);
25+
+ ASYNC_THROW_IF_INSUFFICIENT_PERMISSIONS(
26+
+ env,
27+
+ req_wrap_async,
28+
+ permission::PermissionScope::kFileSystemRead,
29+
+ path.ToStringView());
30+
FS_ASYNC_TRACE_BEGIN1(
31+
UV_FS_REALPATH, req_wrap_async, "path", TRACE_STR_COPY(*path))
32+
AsyncCall(env, req_wrap_async, args, "realpath", encoding, AfterStringPtr,
33+
uv_fs_realpath, *path);
34+
} else { // realpath(path, encoding, undefined, ctx)
35+
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
36+
+ env, permission::PermissionScope::kFileSystemRead, path.ToStringView());
37+
FSReqWrapSync req_wrap_sync("realpath", *path);
38+
FS_SYNC_TRACE_BEGIN(realpath);
39+
int err =
40+
diff --git a/test/fixtures/permission/fs-read.js b/test/fixtures/permission/fs-read.js
41+
index 43e1718a..5051d9c9 100644
42+
--- a/test/fixtures/permission/fs-read.js
43+
+++ b/test/fixtures/permission/fs-read.js
44+
@@ -287,6 +287,19 @@ const regularFile = __filename;
45+
});
46+
}
47+
48+
+// fs.realpath.native
49+
+{
50+
+ fs.realpath.native(blockedFile, common.expectsError({
51+
+ code: 'ERR_ACCESS_DENIED',
52+
+ permission: 'FileSystemRead',
53+
+ resource: path.toNamespacedPath(blockedFile),
54+
+ }));
55+
+
56+
+ // doesNotThrow
57+
+ fs.realpath.native(regularFile, (err) => {
58+
+ assert.ifError(err);
59+
+ });
60+
+}
61+
// fs.watchFile
62+
{
63+
assert.throws(() => {
64+
--
65+
2.45.4
66+

0 commit comments

Comments
 (0)