Skip to content

Commit 6c6d170

Browse files
committed
https: limit proxy CONNECT response headers
Apply the configured maximum header size while reading CONNECT responses. Rearm the readable listener only once per read pass to avoid unbounded buffer and listener growth. Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent cf882a7 commit 6c6d170

2 files changed

Lines changed: 69 additions & 10 deletions

File tree

lib/https.js

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ let debug = require('internal/util/debuglog').debuglog('https', (fn) => {
6666
debug = fn;
6767
});
6868
const net = require('net');
69+
const { Buffer } = require('buffer');
6970
const { URL, urlToHttpOptions, isURL } = require('internal/url');
7071
const { validateObject } = require('internal/validators');
7172
const { isIP } = require('internal/net');
@@ -212,16 +213,18 @@ function getTunnelConfigForProxiedHttps(agent, reqOptions) {
212213

213214
function establishTunnel(agent, socket, options, tunnelConfig, afterSocket) {
214215
const { proxyTunnelPayload } = tunnelConfig;
216+
const maxHeaderSize = tunnelConfig.requestOptions.maxHeaderSize ||
217+
getOptionValue('--max-http-header-size');
215218
// By default, the socket is in paused mode. Read to look for the 200
216219
// connection established response.
217220
function read() {
218221
let chunk;
219222
while ((chunk = socket.read()) !== null) {
220-
if (onProxyData(chunk) !== -1) {
221-
break;
223+
if (onProxyData(chunk)) {
224+
return;
222225
}
223226
}
224-
socket.on('readable', read);
227+
socket.once('readable', read);
225228
}
226229

227230
function cleanup() {
@@ -239,14 +242,25 @@ function establishTunnel(agent, socket, options, tunnelConfig, afterSocket) {
239242

240243
// Read the headers from the chunks and check for the status code. If it fails we
241244
// clean up the socket and return an error. Otherwise we establish the tunnel.
242-
let buffer = '';
245+
let buffer;
243246
function onProxyData(chunk) {
244-
const str = chunk.toString();
245-
debug('onProxyData', str);
246-
buffer += str;
247+
debug('onProxyData', chunk.toString());
248+
buffer = buffer === undefined ? chunk :
249+
Buffer.concat([buffer, chunk], buffer.length + chunk.length);
247250
const headerEndIndex = buffer.indexOf('\r\n\r\n');
248-
if (headerEndIndex === -1) return headerEndIndex;
249-
const statusLine = buffer.substring(0, buffer.indexOf('\r\n'));
251+
const headerLength = headerEndIndex === -1 ?
252+
buffer.length : headerEndIndex + 4;
253+
if (headerLength > maxHeaderSize) {
254+
debug(`Proxy response headers exceed ${maxHeaderSize} bytes, cleaning up`);
255+
cleanup();
256+
const err = new ERR_PROXY_TUNNEL(
257+
`Proxy response headers exceeded ${maxHeaderSize} bytes`);
258+
afterSocket(err, socket);
259+
return true;
260+
}
261+
if (headerEndIndex === -1) return false;
262+
const statusLineEndIndex = buffer.indexOf('\r\n');
263+
const statusLine = buffer.subarray(0, statusLineEndIndex).toString();
250264
const statusCode = statusLine.split(' ')[1];
251265
if (statusCode !== '200') {
252266
debug(`onProxyData receives ${statusCode}, cleaning up`);
@@ -286,7 +300,7 @@ function establishTunnel(agent, socket, options, tunnelConfig, afterSocket) {
286300
});
287301
tunneldSocket.on('error', onTLSHandshakeError);
288302
}
289-
return headerEndIndex;
303+
return true;
290304
}
291305

292306
function onProxyEnd() {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// This tests that the proxy server cannot send unbounded incomplete headers
2+
// while establishing a CONNECT tunnel.
3+
4+
import * as common from '../common/index.mjs';
5+
import assert from 'node:assert';
6+
import { once } from 'events';
7+
import http from 'node:http';
8+
import { runProxiedRequest } from '../common/proxy-server.js';
9+
10+
if (!common.hasCrypto)
11+
common.skip('missing crypto');
12+
13+
const proxy = http.createServer();
14+
proxy.on('connect', common.mustCall((req, socket) => {
15+
socket.write('HTTP/1.1 200 Connection Established\r\n');
16+
17+
const interval = setInterval(() => {
18+
if (socket.destroyed) {
19+
clearInterval(interval);
20+
return;
21+
}
22+
socket.write('x'.repeat(100));
23+
}, 10);
24+
socket.on('close', () => clearInterval(interval));
25+
socket.on('error', () => clearInterval(interval));
26+
}, 1));
27+
proxy.listen(0);
28+
await once(proxy, 'listening');
29+
30+
const { code, signal, stderr, stdout } = await runProxiedRequest({
31+
NODE_USE_ENV_PROXY: 1,
32+
REQUEST_URL: 'https://localhost:1/test',
33+
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
34+
}, ['--max-http-header-size=1024']);
35+
36+
assert.match(
37+
stderr,
38+
/ERR_PROXY_TUNNEL.*Proxy response headers exceeded 1024 bytes/,
39+
);
40+
assert.doesNotMatch(stderr, /MaxListenersExceededWarning/);
41+
assert.strictEqual(stdout.trim(), '');
42+
assert.strictEqual(code, 0);
43+
assert.strictEqual(signal, null);
44+
45+
proxy.close();

0 commit comments

Comments
 (0)