已开启
Fix CVE-2026-56846 #262
Fix CVE-2026-56846 #262
已开启
易大夫创建于 20 天前
共 2 个文件变更+382-0
@@ -0,0 +1,191 @@
1+From f14d78b9e0201bfe0291f2b0dc4b5d88c70cc28e Mon Sep 17 00:00:00 2001
2+From: Matteo Collina <hello@matteocollina.com>
3+Date: Mon, 8 Jun 2026 13:53:02 +0200
4+Subject: [PATCH] http2: retain header memory in session accounting
5+MIME-Version: 1.0
6+Content-Type: text/plain; charset=UTF-8
7+Content-Transfer-Encoding: 8bit
8+ 
9+Signed-off-by: Matteo Collina <hello@matteocollina.com>
10+PR-URL: https://github.com/nodejs/node/pull/63752
11+Reviewed-By: Tim Perry <pimterry@gmail.com>
12+Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
13+Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
14+CVE-ID: CVE-2026-56846
15+---
16+ doc/api/http2.md | 21 +++---
17+ src/node_http2.cc | 13 +++-
18+ src/node_http2.h | 4 +-
19+ ...ttp2-max-session-memory-stalled-headers.js | 69 +++++++++++++++++++
20+ 4 files changed, 96 insertions(+), 11 deletions(-)
21+ create mode 100644 test/parallel/test-http2-max-session-memory-stalled-headers.js
22+ 
23+diff --git a/doc/api/http2.md b/doc/api/http2.md
24+index 2180d0f70e6a..539979aa6d18 100644
25+--- a/doc/api/http2.md
26++++ b/doc/api/http2.md
27+@@ -2845,9 +2845,10 @@ changes:
28+ This is a credit based limit, existing `Http2Stream`s may cause this
29+ limit to be exceeded, but new `Http2Stream` instances will be rejected
30+ while this limit is exceeded. The current number of `Http2Stream` sessions,
31+- the current memory use of the header compression tables, current data
32+- queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
33+- counted towards the current limit. **Default:** `10`.
34++ the current memory use of the header compression tables, header blocks
35++ retained by open streams, current data queued to be sent, and
36++ unacknowledged `PING` and `SETTINGS` frames are all counted towards the
37++ current limit. **Default:** `10`.
38+ * `maxHeaderListPairs` {number} Sets the maximum number of header entries.
39+ This is similar to [`server.maxHeadersCount`][] or
40+ [`request.maxHeadersCount`][] in the `node:http` module. The minimum value
41+@@ -3034,9 +3035,10 @@ changes:
42+ credit based limit, existing `Http2Stream`s may cause this
43+ limit to be exceeded, but new `Http2Stream` instances will be rejected
44+ while this limit is exceeded. The current number of `Http2Stream` sessions,
45+- the current memory use of the header compression tables, current data
46+- queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
47+- counted towards the current limit. **Default:** `10`.
48++ the current memory use of the header compression tables, header blocks
49++ retained by open streams, current data queued to be sent, and
50++ unacknowledged `PING` and `SETTINGS` frames are all counted towards the
51++ current limit. **Default:** `10`.
52+ * `maxHeaderListPairs` {number} Sets the maximum number of header entries.
53+ This is similar to [`server.maxHeadersCount`][] or
54+ [`request.maxHeadersCount`][] in the `node:http` module. The minimum value
55+@@ -3196,9 +3198,10 @@ changes:
56+ This is a credit based limit, existing `Http2Stream`s may cause this
57+ limit to be exceeded, but new `Http2Stream` instances will be rejected
58+ while this limit is exceeded. The current number of `Http2Stream` sessions,
59+- the current memory use of the header compression tables, current data
60+- queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
61+- counted towards the current limit. **Default:** `10`.
62++ the current memory use of the header compression tables, header blocks
63++ retained by open streams, current data queued to be sent, and
64++ unacknowledged `PING` and `SETTINGS` frames are all counted towards the
65++ current limit. **Default:** `10`.
66+ * `maxHeaderListPairs` {number} Sets the maximum number of header entries.
67+ This is similar to [`server.maxHeadersCount`][] or
68+ [`request.maxHeadersCount`][] in the `node:http` module. The minimum value
69+diff --git a/src/node_http2.cc b/src/node_http2.cc
70+index 403d9982ebde..a496e27289d7 100644
71+--- a/src/node_http2.cc
72++++ b/src/node_http2.cc
73+@@ -907,6 +907,14 @@ BaseObjectPtr<Http2Stream> Http2Session::RemoveStream(int32_t id) {
74+ stream = FindStream(id);
75+ if (stream) {
76+ streams_.erase(id);
77++ if (stream->current_headers_length_ > 0) {
78++ DecrementCurrentSessionMemory(stream->current_headers_length_);
79++ stream->current_headers_length_ = 0;
80++ }
81++ if (stream->retained_headers_length_ > 0) {
82++ DecrementCurrentSessionMemory(stream->retained_headers_length_);
83++ stream->retained_headers_length_ = 0;
84++ }
85+ DecrementCurrentSessionMemory(sizeof(*stream));
86+ }
87+ return stream;
88+@@ -1553,7 +1561,10 @@ void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) {
89+ });
90+ CHECK_EQ(stream->headers_count(), 0);
91+
92+- DecrementCurrentSessionMemory(stream->current_headers_length_);
93++ // Keep the header block charged against maxSessionMemory while the
94++ // corresponding JS objects can still keep it alive for the lifetime of
95++ // the stream.
96++ stream->retained_headers_length_ += stream->current_headers_length_;
97+ stream->current_headers_length_ = 0;
98+
99+ Local<Value> args[] = {
100+diff --git a/src/node_http2.h b/src/node_http2.h
101+index 910460529275..cb0e067aeaa1 100644
102+--- a/src/node_http2.h
103++++ b/src/node_http2.h
104+@@ -485,9 +485,11 @@ class Http2Stream : public AsyncWrap,
105+
106+ // The Current Headers block... As headers are received for this stream,
107+ // they are temporarily stored here until the OnFrameReceived is called
108+- // signalling the end of the HEADERS frame
109++ // signalling the end of the HEADERS frame.
110+ nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS;
111+ uint32_t current_headers_length_ = 0; // total number of octets
112++ // Charged against maxSessionMemory while headers stay alive in JS.
113++ uint64_t retained_headers_length_ = 0;
114+ std::vector<Http2Header> current_headers_;
115+
116+ // This keeps track of the amount of data read from the socket while the
117+diff --git a/test/parallel/test-http2-max-session-memory-stalled-headers.js b/test/parallel/test-http2-max-session-memory-stalled-headers.js
118+new file mode 100644
119+index 000000000000..c04bfd09e627
120+--- /dev/null
121++++ b/test/parallel/test-http2-max-session-memory-stalled-headers.js
122+@@ -0,0 +1,69 @@
123++'use strict';
124++
125++const common = require('../common');
126++if (!common.hasCrypto)
127++ common.skip('missing crypto');
128++
129++const Countdown = require('../common/countdown');
130++const assert = require('assert');
131++const http2 = require('http2');
132++
133++const {
134++ NGHTTP2_ENHANCE_YOUR_CALM,
135++} = http2.constants;
136++
137++// Regression test: header blocks retained by stalled streams should continue
138++// to count against maxSessionMemory after they have been handed to JS.
139++const maxSessionMemory = 1;
140++const totalRequests = 400;
141++const cookieCrumbs = 120;
142++
143++let accepted = 0;
144++let rejected = 0;
145++
146++const server = http2.createServer({ maxSessionMemory });
147++server.on('stream', (stream) => {
148++ accepted++;
149++ stream.on('error', () => {});
150++ stream.respond();
151++ stream.write('x');
152++});
153++
154++server.listen(0, common.mustCall(() => {
155++ const client = http2.connect(`http://localhost:${server.address().port}`, {
156++ settings: {
157++ initialWindowSize: 0,
158++ },
159++ });
160++ client.on('error', () => {});
161++
162++ client.on('remoteSettings', common.mustCall(() => {
163++ let destroyed = false;
164++ const countdown = new Countdown(totalRequests, common.mustCall(() => {
165++ assert(rejected > 0);
166++ assert(accepted < totalRequests);
167++ server.close();
168++ }));
169++
170++ for (let i = 0; i < totalRequests; i++) {
171++ const headers = [':path', '/'];
172++ for (let j = 0; j < cookieCrumbs; j++) {
173++ headers.push('cookie', 'a=1');
174++ }
175++
176++ const req = client.request(headers);
177++ req.on('error', () => {});
178++ req.on('close', () => {
179++ if (req.rstCode === NGHTTP2_ENHANCE_YOUR_CALM) {
180++ rejected++;
181++ if (!destroyed) {
182++ destroyed = true;
183++ client.destroy();
184++ }
185++ }
186++ countdown.dec();
187++ });
188++ req.end();
189++ }
190++ }));
191++}));
@@ -0,0 +1,191 @@
1+From 34ed88a069393a40fb97c7015e5539a0818ae756 Mon Sep 17 00:00:00 2001
2+From: Matteo Collina <hello@matteocollina.com>
3+Date: Mon, 8 Jun 2026 13:53:02 +0200
4+Subject: [PATCH] http2: retain header memory in session accounting
5+MIME-Version: 1.0
6+Content-Type: text/plain; charset=UTF-8
7+Content-Transfer-Encoding: 8bit
8+ 
9+Signed-off-by: Matteo Collina <hello@matteocollina.com>
10+PR-URL: https://github.com/nodejs/node/pull/63752
11+Reviewed-By: Tim Perry <pimterry@gmail.com>
12+Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
13+Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
14+CVE-ID: CVE-2026-56846
15+---
16+ doc/api/http2.md | 21 +++---
17+ src/node_http2.cc | 13 +++-
18+ src/node_http2.h | 4 +-
19+ ...ttp2-max-session-memory-stalled-headers.js | 69 +++++++++++++++++++
20+ 4 files changed, 96 insertions(+), 11 deletions(-)
21+ create mode 100644 test/parallel/test-http2-max-session-memory-stalled-headers.js
22+ 
23+diff --git a/doc/api/http2.md b/doc/api/http2.md
24+index fc30d273967d..ca7c4a216404 100644
25+--- a/doc/api/http2.md
26++++ b/doc/api/http2.md
27+@@ -2860,9 +2860,10 @@ changes:
28+ This is a credit based limit, existing `Http2Stream`s may cause this
29+ limit to be exceeded, but new `Http2Stream` instances will be rejected
30+ while this limit is exceeded. The current number of `Http2Stream` sessions,
31+- the current memory use of the header compression tables, current data
32+- queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
33+- counted towards the current limit. **Default:** `10`.
34++ the current memory use of the header compression tables, header blocks
35++ retained by open streams, current data queued to be sent, and
36++ unacknowledged `PING` and `SETTINGS` frames are all counted towards the
37++ current limit. **Default:** `10`.
38+ * `maxHeaderListPairs` {number} Sets the maximum number of header entries.
39+ This is similar to [`server.maxHeadersCount`][] or
40+ [`request.maxHeadersCount`][] in the `node:http` module. The minimum value
41+@@ -3077,9 +3078,10 @@ changes:
42+ credit based limit, existing `Http2Stream`s may cause this
43+ limit to be exceeded, but new `Http2Stream` instances will be rejected
44+ while this limit is exceeded. The current number of `Http2Stream` sessions,
45+- the current memory use of the header compression tables, current data
46+- queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
47+- counted towards the current limit. **Default:** `10`.
48++ the current memory use of the header compression tables, header blocks
49++ retained by open streams, current data queued to be sent, and
50++ unacknowledged `PING` and `SETTINGS` frames are all counted towards the
51++ current limit. **Default:** `10`.
52+ * `maxHeaderListPairs` {number} Sets the maximum number of header entries.
53+ This is similar to [`server.maxHeadersCount`][] or
54+ [`request.maxHeadersCount`][] in the `node:http` module. The minimum value
55+@@ -3257,9 +3259,10 @@ changes:
56+ This is a credit based limit, existing `Http2Stream`s may cause this
57+ limit to be exceeded, but new `Http2Stream` instances will be rejected
58+ while this limit is exceeded. The current number of `Http2Stream` sessions,
59+- the current memory use of the header compression tables, current data
60+- queued to be sent, and unacknowledged `PING` and `SETTINGS` frames are all
61+- counted towards the current limit. **Default:** `10`.
62++ the current memory use of the header compression tables, header blocks
63++ retained by open streams, current data queued to be sent, and
64++ unacknowledged `PING` and `SETTINGS` frames are all counted towards the
65++ current limit. **Default:** `10`.
66+ * `maxHeaderListPairs` {number} Sets the maximum number of header entries.
67+ This is similar to [`server.maxHeadersCount`][] or
68+ [`request.maxHeadersCount`][] in the `node:http` module. The minimum value
69+diff --git a/src/node_http2.cc b/src/node_http2.cc
70+index 55bca557a81e..591798a63715 100644
71+--- a/src/node_http2.cc
72++++ b/src/node_http2.cc
73+@@ -902,6 +902,14 @@ BaseObjectPtr<Http2Stream> Http2Session::RemoveStream(int32_t id) {
74+ stream = FindStream(id);
75+ if (stream) {
76+ streams_.erase(id);
77++ if (stream->current_headers_length_ > 0) {
78++ DecrementCurrentSessionMemory(stream->current_headers_length_);
79++ stream->current_headers_length_ = 0;
80++ }
81++ if (stream->retained_headers_length_ > 0) {
82++ DecrementCurrentSessionMemory(stream->retained_headers_length_);
83++ stream->retained_headers_length_ = 0;
84++ }
85+ DecrementCurrentSessionMemory(sizeof(*stream));
86+ }
87+ return stream;
88+@@ -1548,7 +1556,10 @@ void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) {
89+ });
90+ CHECK_EQ(stream->headers_count(), 0);
91+
92+- DecrementCurrentSessionMemory(stream->current_headers_length_);
93++ // Keep the header block charged against maxSessionMemory while the
94++ // corresponding JS objects can still keep it alive for the lifetime of
95++ // the stream.
96++ stream->retained_headers_length_ += stream->current_headers_length_;
97+ stream->current_headers_length_ = 0;
98+
99+ Local<Value> args[] = {
100+diff --git a/src/node_http2.h b/src/node_http2.h
101+index 28245d7b98e0..0bb2b0cb2891 100644
102+--- a/src/node_http2.h
103++++ b/src/node_http2.h
104+@@ -490,9 +490,11 @@ class Http2Stream : public AsyncWrap,
105+
106+ // The Current Headers block... As headers are received for this stream,
107+ // they are temporarily stored here until the OnFrameReceived is called
108+- // signalling the end of the HEADERS frame
109++ // signalling the end of the HEADERS frame.
110+ nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS;
111+ uint32_t current_headers_length_ = 0; // total number of octets
112++ // Charged against maxSessionMemory while headers stay alive in JS.
113++ uint64_t retained_headers_length_ = 0;
114+ std::vector<Http2Header> current_headers_;
115+
116+ // This keeps track of the amount of data read from the socket while the
117+diff --git a/test/parallel/test-http2-max-session-memory-stalled-headers.js b/test/parallel/test-http2-max-session-memory-stalled-headers.js
118+new file mode 100644
119+index 000000000000..c04bfd09e627
120+--- /dev/null
121++++ b/test/parallel/test-http2-max-session-memory-stalled-headers.js
122+@@ -0,0 +1,69 @@
123++'use strict';
124++
125++const common = require('../common');
126++if (!common.hasCrypto)
127++ common.skip('missing crypto');
128++
129++const Countdown = require('../common/countdown');
130++const assert = require('assert');
131++const http2 = require('http2');
132++
133++const {
134++ NGHTTP2_ENHANCE_YOUR_CALM,
135++} = http2.constants;
136++
137++// Regression test: header blocks retained by stalled streams should continue
138++// to count against maxSessionMemory after they have been handed to JS.
139++const maxSessionMemory = 1;
140++const totalRequests = 400;
141++const cookieCrumbs = 120;
142++
143++let accepted = 0;
144++let rejected = 0;
145++
146++const server = http2.createServer({ maxSessionMemory });
147++server.on('stream', (stream) => {
148++ accepted++;
149++ stream.on('error', () => {});
150++ stream.respond();
151++ stream.write('x');
152++});
153++
154++server.listen(0, common.mustCall(() => {
155++ const client = http2.connect(`http://localhost:${server.address().port}`, {
156++ settings: {
157++ initialWindowSize: 0,
158++ },
159++ });
160++ client.on('error', () => {});
161++
162++ client.on('remoteSettings', common.mustCall(() => {
163++ let destroyed = false;
164++ const countdown = new Countdown(totalRequests, common.mustCall(() => {
165++ assert(rejected > 0);
166++ assert(accepted < totalRequests);
167++ server.close();
168++ }));
169++
170++ for (let i = 0; i < totalRequests; i++) {
171++ const headers = [':path', '/'];
172++ for (let j = 0; j < cookieCrumbs; j++) {
173++ headers.push('cookie', 'a=1');
174++ }
175++
176++ const req = client.request(headers);
177++ req.on('error', () => {});
178++ req.on('close', () => {
179++ if (req.rstCode === NGHTTP2_ENHANCE_YOUR_CALM) {
180++ rejected++;
181++ if (!destroyed) {
182++ destroyed = true;
183++ client.destroy();
184++ }
185++ }
186++ countdown.dec();
187++ });
188++ req.end();
189++ }
190++ }));
191++}));