Skip to content

Commit 6257408

Browse files
addaleaxtargos
authored andcommitted
http: make maximum header size configurable per-stream or per-server
Make `maxHeaderSize` a.k.a. `--max-header-size` configurable now that the legacy parser is gone (which only supported a single global value). Refs: #30567 PR-URL: #30570 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: David Carlier <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Sam Roberts <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Denys Otrishko <[email protected]>
1 parent e968e26 commit 6257408

File tree

7 files changed

+135
-10
lines changed

7 files changed

+135
-10
lines changed

doc/api/http.md

+17
Original file line numberDiff line numberDiff line change
@@ -2047,6 +2047,9 @@ Found'`.
20472047
<!-- YAML
20482048
added: v0.1.13
20492049
changes:
2050+
- version: REPLACEME
2051+
pr-url: https://github.com./nodejs/node/pull/30570
2052+
description: The `maxHeaderSize` option is supported now.
20502053
- version: v9.6.0, v8.12.0
20512054
pr-url: https://github.com./nodejs/node/pull/15752
20522055
description: The `options` argument is supported now.
@@ -2059,6 +2062,10 @@ changes:
20592062
* `ServerResponse` {http.ServerResponse} Specifies the `ServerResponse` class
20602063
to be used. Useful for extending the original `ServerResponse`. **Default:**
20612064
`ServerResponse`.
2065+
* `maxHeaderSize` {number} Optionally overrides the value of
2066+
[`--max-http-header-size`][] for requests received by this server, i.e.
2067+
the maximum length of request headers in bytes.
2068+
**Default:** 8192 (8KB).
20622069
* `requestListener` {Function}
20632070

20642071
* Returns: {http.Server}
@@ -2156,11 +2163,17 @@ added: v11.6.0
21562163
Read-only property specifying the maximum allowed size of HTTP headers in bytes.
21572164
Defaults to 8KB. Configurable using the [`--max-http-header-size`][] CLI option.
21582165

2166+
This can be overridden for servers and client requests by passing the
2167+
`maxHeaderSize` option.
2168+
21592169
## http.request(options\[, callback\])
21602170
## http.request(url\[, options\]\[, callback\])
21612171
<!-- YAML
21622172
added: v0.3.6
21632173
changes:
2174+
- version: REPLACEME
2175+
pr-url: https://github.com./nodejs/node/pull/30570
2176+
description: The `maxHeaderSize` option is supported now.
21642177
- version: v10.9.0
21652178
pr-url: https://github.com./nodejs/node/pull/21616
21662179
description: The `url` parameter can now be passed along with a separate
@@ -2196,6 +2209,10 @@ changes:
21962209
`hostname` will be used if both `host` and `hostname` are specified.
21972210
* `localAddress` {string} Local interface to bind for network connections.
21982211
* `lookup` {Function} Custom lookup function. **Default:** [`dns.lookup()`][].
2212+
* `maxHeaderSize` {number} Optionally overrides the value of
2213+
[`--max-http-header-size`][] for requests received from the server, i.e.
2214+
the maximum length of response headers in bytes.
2215+
**Default:** 8192 (8KB).
21992216
* `method` {string} A string specifying the HTTP request method. **Default:**
22002217
`'GET'`.
22012218
* `path` {string} Request path. Should include query string if any.

lib/_http_client.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const {
5555
ERR_INVALID_PROTOCOL,
5656
ERR_UNESCAPED_CHARACTERS
5757
} = codes;
58+
const { validateInteger } = require('internal/validators');
5859
const { getTimerDuration } = require('internal/timers');
5960
const {
6061
DTRACE_HTTP_CLIENT_REQUEST,
@@ -179,6 +180,11 @@ function ClientRequest(input, options, cb) {
179180
method = this.method = 'GET';
180181
}
181182

183+
const maxHeaderSize = options.maxHeaderSize;
184+
if (maxHeaderSize !== undefined)
185+
validateInteger(maxHeaderSize, 'maxHeaderSize', 0);
186+
this.maxHeaderSize = maxHeaderSize;
187+
182188
this.path = options.path || '/';
183189
if (cb) {
184190
this.once('response', cb);
@@ -662,7 +668,8 @@ function tickOnSocket(req, socket) {
662668
const parser = parsers.alloc();
663669
req.socket = socket;
664670
parser.initialize(HTTPParser.RESPONSE,
665-
new HTTPClientAsyncResource('HTTPINCOMINGMESSAGE', req));
671+
new HTTPClientAsyncResource('HTTPINCOMINGMESSAGE', req),
672+
req.maxHeaderSize || 0);
666673
parser.socket = socket;
667674
parser.outgoing = req;
668675
req.parser = parser;

lib/_http_server.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const {
5858
ERR_INVALID_ARG_TYPE,
5959
ERR_INVALID_CHAR
6060
} = require('internal/errors').codes;
61+
const { validateInteger } = require('internal/validators');
6162
const Buffer = require('buffer').Buffer;
6263
const {
6364
DTRACE_HTTP_SERVER_REQUEST,
@@ -322,6 +323,11 @@ function Server(options, requestListener) {
322323
this[kIncomingMessage] = options.IncomingMessage || IncomingMessage;
323324
this[kServerResponse] = options.ServerResponse || ServerResponse;
324325

326+
const maxHeaderSize = options.maxHeaderSize;
327+
if (maxHeaderSize !== undefined)
328+
validateInteger(maxHeaderSize, 'maxHeaderSize', 0);
329+
this.maxHeaderSize = maxHeaderSize;
330+
325331
net.Server.call(this, { allowHalfOpen: true });
326332

327333
if (requestListener) {
@@ -379,7 +385,8 @@ function connectionListenerInternal(server, socket) {
379385
// https://github.com./nodejs/node/pull/21313
380386
parser.initialize(
381387
HTTPParser.REQUEST,
382-
new HTTPServerAsyncResource('HTTPINCOMINGMESSAGE', socket)
388+
new HTTPServerAsyncResource('HTTPINCOMINGMESSAGE', socket),
389+
server.maxHeaderSize || 0
383390
);
384391
parser.socket = socket;
385392

src/node_http_parser.cc

+15-3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ using v8::Int32;
6262
using v8::Integer;
6363
using v8::Local;
6464
using v8::MaybeLocal;
65+
using v8::Number;
6566
using v8::Object;
6667
using v8::String;
6768
using v8::Uint32;
@@ -486,8 +487,17 @@ class Parser : public AsyncWrap, public StreamListener {
486487
static void Initialize(const FunctionCallbackInfo<Value>& args) {
487488
Environment* env = Environment::GetCurrent(args);
488489

490+
uint64_t max_http_header_size = 0;
491+
489492
CHECK(args[0]->IsInt32());
490493
CHECK(args[1]->IsObject());
494+
if (args.Length() > 2) {
495+
CHECK(args[2]->IsNumber());
496+
max_http_header_size = args[2].As<Number>()->Value();
497+
}
498+
if (max_http_header_size == 0) {
499+
max_http_header_size = env->options()->max_http_header_size;
500+
}
491501

492502
llhttp_type_t type =
493503
static_cast<llhttp_type_t>(args[0].As<Int32>()->Value());
@@ -505,7 +515,7 @@ class Parser : public AsyncWrap, public StreamListener {
505515

506516
parser->set_provider_type(provider);
507517
parser->AsyncReset(args[1].As<Object>());
508-
parser->Init(type);
518+
parser->Init(type, max_http_header_size);
509519
}
510520

511521
template <bool should_pause>
@@ -752,7 +762,7 @@ class Parser : public AsyncWrap, public StreamListener {
752762
}
753763

754764

755-
void Init(llhttp_type_t type) {
765+
void Init(llhttp_type_t type, uint64_t max_http_header_size) {
756766
llhttp_init(&parser_, type, &settings);
757767
header_nread_ = 0;
758768
url_.Reset();
@@ -761,12 +771,13 @@ class Parser : public AsyncWrap, public StreamListener {
761771
num_values_ = 0;
762772
have_flushed_ = false;
763773
got_exception_ = false;
774+
max_http_header_size_ = max_http_header_size;
764775
}
765776

766777

767778
int TrackHeader(size_t len) {
768779
header_nread_ += len;
769-
if (header_nread_ >= per_process::cli_options->max_http_header_size) {
780+
if (header_nread_ >= max_http_header_size_) {
770781
llhttp_set_error_reason(&parser_, "HPE_HEADER_OVERFLOW:Header overflow");
771782
return HPE_USER;
772783
}
@@ -801,6 +812,7 @@ class Parser : public AsyncWrap, public StreamListener {
801812
unsigned int execute_depth_ = 0;
802813
bool pending_pause_ = false;
803814
uint64_t header_nread_ = 0;
815+
uint64_t max_http_header_size_;
804816

805817
// These are helper functions for filling `http_parser_settings`, which turn
806818
// a member function of Parser into a C-style HTTP parser callback.

src/node_options.cc

+4-4
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
436436
"profile generated with --heap-prof. (default: 512 * 1024)",
437437
&EnvironmentOptions::heap_prof_interval);
438438
#endif // HAVE_INSPECTOR
439+
AddOption("--max-http-header-size",
440+
"set the maximum size of HTTP headers (default: 8192 (8KB))",
441+
&EnvironmentOptions::max_http_header_size,
442+
kAllowedInEnvironment);
439443
AddOption("--redirect-warnings",
440444
"write warnings to file instead of stderr",
441445
&EnvironmentOptions::redirect_warnings,
@@ -628,10 +632,6 @@ PerProcessOptionsParser::PerProcessOptionsParser(
628632
kAllowedInEnvironment);
629633
AddAlias("--trace-events-enabled", {
630634
"--trace-event-categories", "v8,node,node.async_hooks" });
631-
AddOption("--max-http-header-size",
632-
"set the maximum size of HTTP headers (default: 8KB)",
633-
&PerProcessOptions::max_http_header_size,
634-
kAllowedInEnvironment);
635635
AddOption("--v8-pool-size",
636636
"set V8's thread pool size",
637637
&PerProcessOptions::v8_thread_pool_size,

src/node_options.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class EnvironmentOptions : public Options {
115115
bool expose_internals = false;
116116
bool frozen_intrinsics = false;
117117
std::string heap_snapshot_signal;
118+
uint64_t max_http_header_size = 8 * 1024;
118119
bool no_deprecation = false;
119120
bool no_force_async_hooks_checks = false;
120121
bool no_warnings = false;
@@ -200,7 +201,6 @@ class PerProcessOptions : public Options {
200201
std::string title;
201202
std::string trace_event_categories;
202203
std::string trace_event_file_pattern = "node_trace.${rotation}.log";
203-
uint64_t max_http_header_size = 8 * 1024;
204204
int64_t v8_thread_pool_size = 4;
205205
bool zero_fill_all_buffers = false;
206206
bool debug_arraybuffer_allocations = false;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const http = require('http');
5+
const MakeDuplexPair = require('../common/duplexpair');
6+
7+
// Test that setting the `maxHeaderSize` option works on a per-stream-basis.
8+
9+
// Test 1: The server sends larger headers than what would otherwise be allowed.
10+
{
11+
const { clientSide, serverSide } = MakeDuplexPair();
12+
13+
const req = http.request({
14+
createConnection: common.mustCall(() => clientSide),
15+
maxHeaderSize: http.maxHeaderSize * 4
16+
}, common.mustCall((res) => {
17+
assert.strictEqual(res.headers.hello, 'A'.repeat(http.maxHeaderSize * 3));
18+
res.resume(); // We don’t actually care about contents.
19+
res.on('end', common.mustCall());
20+
}));
21+
req.end();
22+
23+
serverSide.resume(); // Dump the request
24+
serverSide.end('HTTP/1.1 200 OK\r\n' +
25+
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
26+
'Content-Length: 0\r\n' +
27+
'\r\n\r\n');
28+
}
29+
30+
// Test 2: The same as Test 1 except without the option, to make sure it fails.
31+
{
32+
const { clientSide, serverSide } = MakeDuplexPair();
33+
34+
const req = http.request({
35+
createConnection: common.mustCall(() => clientSide)
36+
}, common.mustNotCall());
37+
req.end();
38+
req.on('error', common.mustCall());
39+
40+
serverSide.resume(); // Dump the request
41+
serverSide.end('HTTP/1.1 200 OK\r\n' +
42+
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
43+
'Content-Length: 0\r\n' +
44+
'\r\n\r\n');
45+
}
46+
47+
// Test 3: The client sends larger headers than what would otherwise be allowed.
48+
{
49+
const testData = 'Hello, World!\n';
50+
const server = http.createServer(
51+
{ maxHeaderSize: http.maxHeaderSize * 4 },
52+
common.mustCall((req, res) => {
53+
res.statusCode = 200;
54+
res.setHeader('Content-Type', 'text/plain');
55+
res.end(testData);
56+
}));
57+
58+
server.on('clientError', common.mustNotCall());
59+
60+
const { clientSide, serverSide } = MakeDuplexPair();
61+
serverSide.server = server;
62+
server.emit('connection', serverSide);
63+
64+
clientSide.write('GET / HTTP/1.1\r\n' +
65+
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
66+
'\r\n\r\n');
67+
}
68+
69+
// Test 4: The same as Test 3 except without the option, to make sure it fails.
70+
{
71+
const server = http.createServer(common.mustNotCall());
72+
73+
server.on('clientError', common.mustCall());
74+
75+
const { clientSide, serverSide } = MakeDuplexPair();
76+
serverSide.server = server;
77+
server.emit('connection', serverSide);
78+
79+
clientSide.write('GET / HTTP/1.1\r\n' +
80+
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
81+
'\r\n\r\n');
82+
}

0 commit comments

Comments
 (0)