Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: add --max-http-header-size flag #24811

Merged
merged 3 commits into from Dec 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 11 additions & 4 deletions deps/http_parser/http_parser.c
Expand Up @@ -25,6 +25,8 @@
#include <string.h>
#include <limits.h>

static uint32_t max_header_size = HTTP_MAX_HEADER_SIZE;

#ifndef ULLONG_MAX
# define ULLONG_MAX ((uint64_t) -1) /* 2^64-1 */
#endif
Expand Down Expand Up @@ -137,20 +139,20 @@ do { \
} while (0)

/* Don't allow the total size of the HTTP headers (including the status
* line) to exceed HTTP_MAX_HEADER_SIZE. This check is here to protect
* line) to exceed max_header_size. This check is here to protect
* embedders against denial-of-service attacks where the attacker feeds
* us a never-ending header that the embedder keeps buffering.
*
* This check is arguably the responsibility of embedders but we're doing
* it on the embedder's behalf because most won't bother and this way we
* make the web a little safer. HTTP_MAX_HEADER_SIZE is still far bigger
* make the web a little safer. max_header_size is still far bigger
* than any reasonable request or response so this should never affect
* day-to-day operation.
*/
#define COUNT_HEADER_SIZE(V) \
do { \
parser->nread += (V); \
if (UNLIKELY(parser->nread > (HTTP_MAX_HEADER_SIZE))) { \
if (UNLIKELY(parser->nread > max_header_size)) { \
SET_ERRNO(HPE_HEADER_OVERFLOW); \
goto error; \
} \
Expand Down Expand Up @@ -1471,7 +1473,7 @@ size_t http_parser_execute (http_parser *parser,
const char* p_lf;
size_t limit = data + len - p;

limit = MIN(limit, HTTP_MAX_HEADER_SIZE);
limit = MIN(limit, max_header_size);

p_cr = (const char*) memchr(p, CR, limit);
p_lf = (const char*) memchr(p, LF, limit);
Expand Down Expand Up @@ -2437,3 +2439,8 @@ http_parser_version(void) {
HTTP_PARSER_VERSION_MINOR * 0x00100 |
HTTP_PARSER_VERSION_PATCH * 0x00001;
}

void
http_parser_set_max_header_size(uint32_t size) {
max_header_size = size;
}
3 changes: 3 additions & 0 deletions deps/http_parser/http_parser.h
Expand Up @@ -427,6 +427,9 @@ void http_parser_pause(http_parser *parser, int paused);
/* Checks if this is the final chunk of the body. */
int http_body_is_final(const http_parser *parser);

/* Change the maximum header size provided at compile time. */
void http_parser_set_max_header_size(uint32_t size);

#ifdef __cplusplus
}
#endif
Expand Down
8 changes: 8 additions & 0 deletions doc/api/cli.md
Expand Up @@ -197,6 +197,13 @@ added: v9.0.0

Specify the `file` of the custom [experimental ECMAScript Module][] loader.

### `--max-http-header-size=size`
<!-- YAML
added: REPLACEME
-->

Specify the maximum size, in bytes, of HTTP headers. Defaults to 8KB.

### `--napi-modules`
<!-- YAML
added: v7.10.0
Expand Down Expand Up @@ -620,6 +627,7 @@ Node.js options that are allowed are:
- `--inspect-brk`
- `--inspect-port`
- `--loader`
- `--max-http-header-size`
- `--napi-modules`
- `--no-deprecation`
- `--no-force-async-hooks-checks`
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Expand Up @@ -139,6 +139,9 @@ Specify the
as a custom loader, to load
.Fl -experimental-modules .
.
.It Fl -max-http-header-size Ns = Ns Ar size
Specify the maximum size of HTTP headers in bytes. Defaults to 8KB.
.
.It Fl -napi-modules
This option is a no-op.
It is kept for compatibility.
Expand Down
1 change: 1 addition & 0 deletions lib/internal/print_help.js
Expand Up @@ -69,6 +69,7 @@ function getArgDescription(type) {
case 'kHostPort':
return '[host:]port';
case 'kInteger':
case 'kUInteger':
case 'kString':
case 'kStringList':
return '...';
Expand Down
18 changes: 14 additions & 4 deletions src/node_http_parser_impl.h
Expand Up @@ -830,7 +830,7 @@ class Parser : public AsyncWrap, public StreamListener {
int TrackHeader(size_t len) {
#ifdef NODE_EXPERIMENTAL_HTTP
header_nread_ += len;
if (header_nread_ >= kMaxHeaderSize) {
if (header_nread_ >= per_process_opts->max_http_header_size) {
llhttp_set_error_reason(&parser_, "HPE_HEADER_OVERFLOW:Header overflow");
return HPE_USER;
}
Expand Down Expand Up @@ -892,9 +892,6 @@ class Parser : public AsyncWrap, public StreamListener {
typedef int (Parser::*DataCall)(const char* at, size_t length);

static const parser_settings_t settings;
#ifdef NODE_EXPERIMENTAL_HTTP
static const uint64_t kMaxHeaderSize = 8 * 1024;
#endif /* NODE_EXPERIMENTAL_HTTP */
};

const parser_settings_t Parser::settings = {
Expand All @@ -916,6 +913,14 @@ const parser_settings_t Parser::settings = {
};


#ifndef NODE_EXPERIMENTAL_HTTP
void InitMaxHttpHeaderSizeOnce() {
const uint32_t max_http_header_size = per_process_opts->max_http_header_size;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @cjihrig I tried to dig but couldn't figure out the answer.

Does per_process get populated with .npmrc or system environment values? Or can this only be set with the CLI option?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can configure this via the CLI option or NODE_OPTIONS environment variable (search the tests in this PR for NODE_OPTIONS).

http_parser_set_max_header_size(max_http_header_size);
}
#endif /* NODE_EXPERIMENTAL_HTTP */


void InitializeHttpParser(Local<Object> target,
Local<Value> unused,
Local<Context> context,
Expand Down Expand Up @@ -965,6 +970,11 @@ void InitializeHttpParser(Local<Object> target,
target->Set(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "HTTPParser"),
t->GetFunction(env->context()).ToLocalChecked()).FromJust();

#ifndef NODE_EXPERIMENTAL_HTTP
static uv_once_t init_once = UV_ONCE_INIT;
uv_once(&init_once, InitMaxHttpHeaderSizeOnce);
#endif /* NODE_EXPERIMENTAL_HTTP */
}

} // anonymous namespace
Expand Down
16 changes: 16 additions & 0 deletions src/node_options-inl.h
Expand Up @@ -39,6 +39,19 @@ void OptionsParser<Options>::AddOption(const std::string& name,
help_text});
}

template <typename Options>
void OptionsParser<Options>::AddOption(const std::string& name,
const std::string& help_text,
uint64_t Options::* field,
OptionEnvvarSettings env_setting) {
options_.emplace(
name,
OptionInfo{kUInteger,
std::make_shared<SimpleOptionField<uint64_t>>(field),
env_setting,
help_text});
}

template <typename Options>
void OptionsParser<Options>::AddOption(const std::string& name,
const std::string& help_text,
Expand Down Expand Up @@ -401,6 +414,9 @@ void OptionsParser<Options>::Parse(
case kInteger:
*Lookup<int64_t>(info.field, options) = std::atoll(value.c_str());
break;
case kUInteger:
*Lookup<uint64_t>(info.field, options) = std::stoull(value.c_str());
break;
case kString:
*Lookup<std::string>(info.field, options) = value;
break;
Expand Down
8 changes: 8 additions & 0 deletions src/node_options.cc
Expand Up @@ -259,6 +259,10 @@ PerProcessOptionsParser::PerProcessOptionsParser() {
kAllowedInEnvironment);
AddAlias("--trace-events-enabled", {
"--trace-event-categories", "v8,node,node.async_hooks" });
AddOption("--max-http-header-size",
"set the maximum size of HTTP headers (default: 8KB)",
&PerProcessOptions::max_http_header_size,
kAllowedInEnvironment);
AddOption("--v8-pool-size",
"set V8's thread pool size",
&PerProcessOptions::v8_thread_pool_size,
Expand Down Expand Up @@ -429,6 +433,9 @@ void GetOptions(const FunctionCallbackInfo<Value>& args) {
case kInteger:
value = Number::New(isolate, *parser.Lookup<int64_t>(field, opts));
break;
case kUInteger:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s also a list of exported constants which includes the other enum values below, fwiw

value = Number::New(isolate, *parser.Lookup<uint64_t>(field, opts));
break;
case kString:
if (!ToV8Value(context, *parser.Lookup<std::string>(field, opts))
.ToLocal(&value)) {
Expand Down Expand Up @@ -516,6 +523,7 @@ void Initialize(Local<Object> target,
NODE_DEFINE_CONSTANT(types, kV8Option);
NODE_DEFINE_CONSTANT(types, kBoolean);
NODE_DEFINE_CONSTANT(types, kInteger);
NODE_DEFINE_CONSTANT(types, kUInteger);
NODE_DEFINE_CONSTANT(types, kString);
NODE_DEFINE_CONSTANT(types, kHostPort);
NODE_DEFINE_CONSTANT(types, kStringList);
Expand Down
6 changes: 6 additions & 0 deletions src/node_options.h
Expand Up @@ -151,6 +151,7 @@ class PerProcessOptions : public Options {
std::string title;
std::string trace_event_categories;
std::string trace_event_file_pattern = "node_trace.${rotation}.log";
uint64_t max_http_header_size = 8 * 1024;
int64_t v8_thread_pool_size = 4;
bool zero_fill_all_buffers = false;

Expand Down Expand Up @@ -203,6 +204,7 @@ enum OptionType {
kV8Option,
kBoolean,
kInteger,
kUInteger,
kString,
kHostPort,
kStringList,
Expand All @@ -229,6 +231,10 @@ class OptionsParser {
const std::string& help_text,
bool Options::* field,
OptionEnvvarSettings env_setting = kDisallowedInEnvironment);
void AddOption(const std::string& name,
const std::string& help_text,
uint64_t Options::* field,
OptionEnvvarSettings env_setting = kDisallowedInEnvironment);
void AddOption(const std::string& name,
const std::string& help_text,
int64_t Options::* field,
Expand Down
69 changes: 45 additions & 24 deletions test/sequential/test-http-max-http-headers.js
Expand Up @@ -4,10 +4,14 @@ const common = require('../common');
const assert = require('assert');
const http = require('http');
const net = require('net');
const MAX = 8 * 1024; // 8KB
const MAX = +(process.argv[2] || 8 * 1024); // Command line option, or 8KB.

const { getOptionValue } = require('internal/options');

console.log('pid is', process.pid);
console.log('max header size is', getOptionValue('--max-http-header-size'));
console.log('current http parser is', getOptionValue('--http-parser'));

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

function once(cb) {
Expand Down Expand Up @@ -38,19 +42,15 @@ function fillHeaders(headers, currentSize, valid = false) {

// Generate valid headers
if (valid) {
// TODO(mcollina): understand why -9 is needed instead of -1
headers = headers.slice(0, -9);
// TODO(mcollina): understand why -32 is needed instead of -1
headers = headers.slice(0, -32);
}
return headers + '\r\n\r\n';
}

const timeout = common.platformTimeout(10);

function writeHeaders(socket, headers) {
const array = [];

// This is off from 1024 so that \r\n does not get split
const chunkSize = 1000;
const chunkSize = 100;
let last = 0;

for (let i = 0; i < headers.length / chunkSize; i++) {
Expand All @@ -65,19 +65,25 @@ function writeHeaders(socket, headers) {
next();

function next() {
if (socket.write(array.shift())) {
if (array.length === 0) {
socket.end();
} else {
setTimeout(next, timeout);
}
if (socket.destroyed) {
console.log('socket was destroyed early, data left to write:',
array.join('').length);
return;
}

const chunk = array.shift();

if (chunk) {
console.log('writing chunk of size', chunk.length);
socket.write(chunk, next);
} else {
socket.once('drain', next);
socket.end();
}
}
}

function test1() {
console.log('test1');
let headers =
'HTTP/1.1 200 OK\r\n' +
'Content-Length: 0\r\n' +
Expand All @@ -92,6 +98,9 @@ function test1() {
writeHeaders(sock, headers);
sock.resume();
});

// The socket might error but that's ok
sock.on('error', () => {});
});

server.listen(0, common.mustCall(() => {
Expand All @@ -100,17 +109,17 @@ function test1() {

client.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
server.close();
setImmediate(test2);
server.close(test2);
}));
}));
}

const test2 = common.mustCall(() => {
console.log('test2');
let headers =
'GET / HTTP/1.1\r\n' +
'Host: localhost\r\n' +
'Agent: node\r\n' +
'Agent: nod2\r\n' +
'X-CRASH: ';

// /, Host, localhost, Agent, node, X-CRASH, a...
Expand All @@ -119,7 +128,7 @@ const test2 = common.mustCall(() => {

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

server.on('clientError', common.mustCall((err) => {
server.once('clientError', common.mustCall((err) => {
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
}));

Expand All @@ -131,34 +140,46 @@ const test2 = common.mustCall(() => {
});

finished(client, common.mustCall((err) => {
server.close();
setImmediate(test3);
server.close(test3);
}));
}));
});

const test3 = common.mustCall(() => {
console.log('test3');
let headers =
'GET / HTTP/1.1\r\n' +
'Host: localhost\r\n' +
'Agent: node\r\n' +
'Agent: nod3\r\n' +
'X-CRASH: ';

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

console.log('writing', headers.length);

const server = http.createServer(common.mustCall((req, res) => {
res.end('hello world');
setImmediate(server.close.bind(server));
res.end('hello from test3 server');
server.close();
}));

server.on('clientError', (err) => {
console.log(err.code);
if (err.code === 'HPE_HEADER_OVERFLOW') {
console.log(err.rawPacket.toString('hex'));
}
});
server.on('clientError', common.mustNotCall());

server.listen(0, common.mustCall(() => {
const client = net.connect(server.address().port);
client.on('connect', () => {
writeHeaders(client, headers);
client.resume();
});

client.pipe(process.stdout);
}));
});

Expand Down