Skip to content

Commit 693e362

Browse files
cjihrigmcollina
authored andcommittedDec 22, 2018
cli: add --max-http-header-size flag
Allow the maximum size of HTTP headers to be overridden from the command line. Backport-PR-URL: #25171 co-authored-by: Matteo Collina <hello@matteocollina.com> PR-URL: #24811 Fixes: #24692 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Myles Borins <myles.borins@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com> Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
1 parent 4fb5a1b commit 693e362

8 files changed

+183
-24
lines changed
 

‎doc/api/cli.md

+8
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,13 @@ Indicate the end of node options. Pass the rest of the arguments to the script.
405405
If no script filename or eval/print script is supplied prior to this, then
406406
the next argument will be used as a script filename.
407407

408+
### `--max-http-header-size=size`
409+
<!-- YAML
410+
added: REPLACEME
411+
-->
412+
413+
Specify the maximum size, in bytes, of HTTP headers. Defaults to 8KB.
414+
408415
## Environment Variables
409416

410417
### `NODE_DEBUG=module[,…]`
@@ -472,6 +479,7 @@ Node.js options that are allowed are:
472479
- `--inspect-brk`
473480
- `--inspect-port`
474481
- `--inspect`
482+
- `--max-http-header-size`
475483
- `--no-deprecation`
476484
- `--no-warnings`
477485
- `--openssl-config`

‎doc/node.1

+4
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ Activate inspector on host:port and break at start of user script.
110110
.BR \-\-inspect-port \fI=[host:]port\fR
111111
Set the host:port to be used when the inspector is activated.
112112

113+
.TP
114+
.BR \-\-max\-http\-header-size \fI=size\fR
115+
Specify the maximum size of HTTP headers in bytes. Defaults to 8KB.
116+
113117
.TP
114118
.BR \-\-no\-deprecation
115119
Silence deprecation warnings.

‎src/node.cc

+7
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ unsigned int reverted = 0;
201201
std::string icu_data_dir; // NOLINT(runtime/string)
202202
#endif
203203

204+
uint64_t max_http_header_size = 8 * 1024;
205+
204206
// used by C++ modules as well
205207
bool no_deprecation = false;
206208

@@ -3120,6 +3122,8 @@ static void PrintHelp() {
31203122
" set host:port for inspector\n"
31213123
#endif
31223124
" --no-deprecation silence deprecation warnings\n"
3125+
" --max-http-header-size Specify the maximum size of HTTP\n"
3126+
" headers in bytes. Defaults to 8KB.\n"
31233127
" --trace-deprecation show stack traces on deprecations\n"
31243128
" --throw-deprecation throw an exception on deprecations\n"
31253129
" --pending-deprecation emit pending deprecation warnings\n"
@@ -3264,6 +3268,7 @@ static void CheckIfAllowedInEnv(const char* exe, bool is_env,
32643268
"--expose-http2",
32653269
"--experimental-modules",
32663270
"--loader",
3271+
"--max-http-header-size",
32673272
"--trace-warnings",
32683273
"--redirect-warnings",
32693274
"--trace-sync-io",
@@ -3468,6 +3473,8 @@ static void ParseArgs(int* argc,
34683473
new_v8_argc += 1;
34693474
} else if (strncmp(arg, "--v8-pool-size=", 15) == 0) {
34703475
v8_thread_pool_size = atoi(arg + 15);
3476+
} else if (strncmp(arg, "--max-http-header-size=", 23) == 0) {
3477+
max_http_header_size = atoi(arg + 23);
34713478
#if HAVE_OPENSSL
34723479
} else if (strncmp(arg, "--tls-cipher-list=", 18) == 0) {
34733480
default_cipher_list = arg + 18;

‎src/node_config.cc

+4
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ static void InitConfig(Local<Object> target,
8888
if (env->abort_on_uncaught_exception())
8989
READONLY_BOOLEAN_PROPERTY("shouldAbortOnUncaughtException");
9090

91+
READONLY_PROPERTY(target,
92+
"maxHTTPHeaderSize",
93+
Number::New(env->isolate(), max_http_header_size));
94+
9195
READONLY_PROPERTY(target,
9296
"bits",
9397
Number::New(env->isolate(), 8 * sizeof(intptr_t)));

‎src/node_http_parser.cc

+5
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,9 @@ const struct http_parser_settings Parser::settings = {
755755
nullptr // on_chunk_complete
756756
};
757757

758+
void InitMaxHttpHeaderSizeOnce() {
759+
http_parser_set_max_header_size(max_http_header_size);
760+
}
758761

759762
void InitHttpParser(Local<Object> target,
760763
Local<Value> unused,
@@ -801,6 +804,8 @@ void InitHttpParser(Local<Object> target,
801804

802805
target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "HTTPParser"),
803806
t->GetFunction());
807+
static uv_once_t init_once = UV_ONCE_INIT;
808+
uv_once(&init_once, InitMaxHttpHeaderSizeOnce);
804809
}
805810

806811
} // anonymous namespace

‎src/node_internals.h

+3
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ extern std::string config_warning_file; // NOLINT(runtime/string)
176176
// NODE_PENDING_DEPRECATION is used
177177
extern bool config_pending_deprecation;
178178

179+
// Set in node.cc by ParseArgs when --max-http-header-size is used
180+
extern uint64_t max_http_header_size;
181+
179182
// Tells whether it is safe to call v8::Isolate::GetCurrent().
180183
extern bool v8_initialized;
181184

‎test/sequential/test-http-max-http-headers.js

+48-24
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
'use strict';
2+
// Flags: --expose_internals
23

34
const assert = require('assert');
45
const common = require('../common');
56
const http = require('http');
67
const net = require('net');
7-
const MAX = 8 * 1024; // 8KB
8+
const MAX = +(process.argv[2] || 8 * 1024); // Command line option, or 8KB.
9+
10+
assert(process.binding('config').maxHTTPHeaderSize,
11+
'The option should exist on process.binding(\'config\')');
12+
13+
console.log('pid is', process.pid);
14+
console.log('max header size is', process.binding('config').maxHTTPHeaderSize);
815

916
// Verify that we cannot receive more than 8KB of headers.
1017

@@ -28,19 +35,15 @@ function fillHeaders(headers, currentSize, valid = false) {
2835
headers += 'a'.repeat(MAX - headers.length - 3);
2936
// Generate valid headers
3037
if (valid) {
31-
// TODO(mcollina): understand why -9 is needed instead of -1
32-
headers = headers.slice(0, -9);
38+
// TODO(mcollina): understand why -32 is needed instead of -1
39+
headers = headers.slice(0, -32);
3340
}
3441
return headers + '\r\n\r\n';
3542
}
3643

37-
const timeout = common.platformTimeout(10);
38-
3944
function writeHeaders(socket, headers) {
4045
const array = [];
41-
42-
// this is off from 1024 so that \r\n does not get split
43-
const chunkSize = 1000;
46+
const chunkSize = 100;
4447
let last = 0;
4548

4649
for (let i = 0; i < headers.length / chunkSize; i++) {
@@ -55,19 +58,25 @@ function writeHeaders(socket, headers) {
5558
next();
5659

5760
function next() {
58-
if (socket.write(array.shift())) {
59-
if (array.length === 0) {
60-
socket.end();
61-
} else {
62-
setTimeout(next, timeout);
63-
}
61+
if (socket.destroyed) {
62+
console.log('socket was destroyed early, data left to write:',
63+
array.join('').length);
64+
return;
65+
}
66+
67+
const chunk = array.shift();
68+
69+
if (chunk) {
70+
console.log('writing chunk of size', chunk.length);
71+
socket.write(chunk, next);
6472
} else {
65-
socket.once('drain', next);
73+
socket.end();
6674
}
6775
}
6876
}
6977

7078
function test1() {
79+
console.log('test1');
7180
let headers =
7281
'HTTP/1.1 200 OK\r\n' +
7382
'Content-Length: 0\r\n' +
@@ -82,6 +91,9 @@ function test1() {
8291
writeHeaders(sock, headers);
8392
sock.resume();
8493
});
94+
95+
// The socket might error but that's ok
96+
sock.on('error', () => {});
8597
});
8698

8799
server.listen(0, common.mustCall(() => {
@@ -90,17 +102,17 @@ function test1() {
90102

91103
client.on('error', common.mustCall((err) => {
92104
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
93-
server.close();
94-
setImmediate(test2);
105+
server.close(test2);
95106
}));
96107
}));
97108
}
98109

99110
const test2 = common.mustCall(() => {
111+
console.log('test2');
100112
let headers =
101113
'GET / HTTP/1.1\r\n' +
102114
'Host: localhost\r\n' +
103-
'Agent: node\r\n' +
115+
'Agent: nod2\r\n' +
104116
'X-CRASH: ';
105117

106118
// /, Host, localhost, Agent, node, X-CRASH, a...
@@ -109,7 +121,7 @@ const test2 = common.mustCall(() => {
109121

110122
const server = http.createServer(common.mustNotCall());
111123

112-
server.on('clientError', common.mustCall((err) => {
124+
server.once('clientError', common.mustCall((err) => {
113125
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
114126
}));
115127

@@ -121,34 +133,46 @@ const test2 = common.mustCall(() => {
121133
});
122134

123135
finished(client, common.mustCall((err) => {
124-
server.close();
125-
setImmediate(test3);
136+
server.close(test3);
126137
}));
127138
}));
128139
});
129140

130141
const test3 = common.mustCall(() => {
142+
console.log('test3');
131143
let headers =
132144
'GET / HTTP/1.1\r\n' +
133145
'Host: localhost\r\n' +
134-
'Agent: node\r\n' +
146+
'Agent: nod3\r\n' +
135147
'X-CRASH: ';
136148

137149
// /, Host, localhost, Agent, node, X-CRASH, a...
138150
const currentSize = 1 + 4 + 9 + 5 + 4 + 7;
139151
headers = fillHeaders(headers, currentSize, true);
140152

153+
console.log('writing', headers.length);
154+
141155
const server = http.createServer(common.mustCall((req, res) => {
142-
res.end('hello world');
143-
setImmediate(server.close.bind(server));
156+
res.end('hello from test3 server');
157+
server.close();
144158
}));
145159

160+
server.on('clientError', (err) => {
161+
console.log(err.code);
162+
if (err.code === 'HPE_HEADER_OVERFLOW') {
163+
console.log(err.rawPacket.toString('hex'));
164+
}
165+
});
166+
server.on('clientError', common.mustNotCall());
167+
146168
server.listen(0, common.mustCall(() => {
147169
const client = net.connect(server.address().port);
148170
client.on('connect', () => {
149171
writeHeaders(client, headers);
150172
client.resume();
151173
});
174+
175+
client.pipe(process.stdout);
152176
}));
153177
});
154178

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { spawn } = require('child_process');
6+
const path = require('path');
7+
const testName = path.join(__dirname, 'test-http-max-http-headers.js');
8+
9+
const timeout = common.platformTimeout(100);
10+
11+
const tests = [];
12+
13+
function test(fn) {
14+
tests.push(fn);
15+
}
16+
17+
test(function(cb) {
18+
console.log('running subtest expecting failure');
19+
20+
// Validate that the test fails if the max header size is too small.
21+
const args = ['--expose-internals',
22+
'--max-http-header-size=1024',
23+
testName];
24+
const cp = spawn(process.execPath, args, { stdio: 'inherit' });
25+
26+
cp.on('close', common.mustCall((code, signal) => {
27+
assert.strictEqual(code, 1);
28+
assert.strictEqual(signal, null);
29+
cb();
30+
}));
31+
});
32+
33+
test(function(cb) {
34+
console.log('running subtest expecting success');
35+
36+
const env = Object.assign({}, process.env, {
37+
NODE_DEBUG: 'http'
38+
});
39+
40+
// Validate that the test fails if the max header size is too small.
41+
// Validate that the test now passes if the same limit becomes large enough.
42+
const args = ['--expose-internals',
43+
'--max-http-header-size=1024',
44+
testName,
45+
'1024'];
46+
const cp = spawn(process.execPath, args, {
47+
env,
48+
stdio: 'inherit'
49+
});
50+
51+
cp.on('close', common.mustCall((code, signal) => {
52+
assert.strictEqual(code, 0);
53+
assert.strictEqual(signal, null);
54+
cb();
55+
}));
56+
});
57+
58+
// Next, repeat the same checks using NODE_OPTIONS if it is supported.
59+
if (process.config.variables.node_without_node_options) {
60+
const env = Object.assign({}, process.env, {
61+
NODE_OPTIONS: '--max-http-header-size=1024'
62+
});
63+
64+
test(function(cb) {
65+
console.log('running subtest expecting failure');
66+
67+
// Validate that the test fails if the max header size is too small.
68+
const args = ['--expose-internals', testName];
69+
const cp = spawn(process.execPath, args, { env, stdio: 'inherit' });
70+
71+
cp.on('close', common.mustCall((code, signal) => {
72+
assert.strictEqual(code, 1);
73+
assert.strictEqual(signal, null);
74+
cb();
75+
}));
76+
});
77+
78+
test(function(cb) {
79+
// Validate that the test now passes if the same limit
80+
// becomes large enough.
81+
const args = ['--expose-internals', testName, '1024'];
82+
const cp = spawn(process.execPath, args, { env, stdio: 'inherit' });
83+
84+
cp.on('close', common.mustCall((code, signal) => {
85+
assert.strictEqual(code, 0);
86+
assert.strictEqual(signal, null);
87+
cb();
88+
}));
89+
});
90+
}
91+
92+
function runTest() {
93+
const fn = tests.shift();
94+
95+
if (!fn) {
96+
return;
97+
}
98+
99+
fn(() => {
100+
setTimeout(runTest, timeout);
101+
});
102+
}
103+
104+
runTest();

0 commit comments

Comments
 (0)
Please sign in to comment.