Skip to content

Commit 23b94c8

Browse files
mcollinaRafaelGSS
authored andcommitted
http2: defer rst stream while in scope
Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: nodejs-private/node-private#921 Refs: https://hackerone.com/reports/3833629 Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> CVE-ID: CVE-2026-56848
1 parent 3cb607d commit 23b94c8

2 files changed

Lines changed: 78 additions & 2 deletions

File tree

src/node_http2.cc

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2530,10 +2530,18 @@ void Http2Stream::SubmitRstStream(const uint32_t code) {
25302530
// if RST_STREAM received is not in scope and added to the list
25312531
// causing endpoint to hang.
25322532
if (session_->is_in_scope() && is_stream_cancel(code)) {
2533-
session_->AddPendingRstStream(id_);
2534-
return;
2533+
session_->AddPendingRstStream(id_);
2534+
return;
25352535
}
25362536

2537+
// If RST_STREAM is submitted while nghttp2 is processing callbacks for
2538+
// a refused stream, don't force purge pending data. Sending pending data
2539+
// here can re-enter nghttp2 and close streams that are still being used
2540+
// by the active receive operation.
2541+
if (session_->is_in_scope() && code == NGHTTP2_REFUSED_STREAM) {
2542+
FlushRstStream();
2543+
return;
2544+
}
25372545

25382546
// If possible, force a purge of any currently pending data here to make sure
25392547
// it is sent before closing the stream. If it returns non-zero then we need
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto)
4+
common.skip('missing crypto');
5+
6+
const http2 = require('http2');
7+
const net = require('net');
8+
9+
const PREFACE = Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n');
10+
11+
function frame(type, flags, sid, payload = Buffer.alloc(0)) {
12+
const header = Buffer.alloc(9);
13+
header.writeUIntBE(payload.length, 0, 3);
14+
header[3] = type;
15+
header[4] = flags;
16+
header.writeUInt32BE(sid & 0x7fffffff, 5);
17+
return Buffer.concat([header, payload]);
18+
}
19+
20+
function goaway(lastStreamID, code) {
21+
const payload = Buffer.alloc(8);
22+
payload.writeUInt32BE(lastStreamID, 0);
23+
payload.writeUInt32BE(code, 4);
24+
return frame(7, 0, 0, payload);
25+
}
26+
27+
function headers(sid) {
28+
return frame(1, 0x05, sid, Buffer.from([
29+
0x82, // :method: GET
30+
0x86, // :scheme: http
31+
0x84, // :path: /
32+
0x41, 0x01, 0x78, // :authority: x
33+
]));
34+
}
35+
36+
const server = http2.createServer();
37+
38+
server.listen(0, common.mustCall(() => {
39+
const socket = net.connect(server.address().port);
40+
let sent = false;
41+
42+
socket.on('connect', common.mustCall(() => {
43+
socket.write(Buffer.concat([PREFACE, frame(4, 0, 0)]));
44+
}));
45+
46+
socket.on('error', () => {});
47+
48+
socket.on('data', common.mustCallAtLeast(() => {
49+
if (sent)
50+
return;
51+
sent = true;
52+
53+
socket.write(frame(4, 1, 0));
54+
socket.write(headers(1));
55+
56+
setImmediate(() => {
57+
socket.write(Buffer.concat([
58+
goaway(0, 0),
59+
headers(3),
60+
headers(5),
61+
headers(7),
62+
]));
63+
setTimeout(() => socket.destroy(), common.platformTimeout(50));
64+
});
65+
}));
66+
67+
socket.on('close', common.mustCall(() => server.close()));
68+
}));

0 commit comments

Comments
 (0)