http2: implement support for max settings entries

Adds the maxSettings option to limit the number of settings
entries allowed per SETTINGS frame. Default 32

Fixes: https://hackerone.com/reports/446662
CVE-ID: CVE-2020-11080
PR-URL: https://github.com/nodejs-private/node-private/pull/204
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
James M Snell 2020-04-27 10:47:58 -07:00 committed by Michaël Zasso
parent d3beb50da3
commit 3948830ce6
No known key found for this signature in database
GPG Key ID: 770F7A9A5AE15600
6 changed files with 70 additions and 3 deletions

View File

@ -2001,6 +2001,9 @@ value only affects new connections to the server, not any existing connections.
<!-- YAML
added: v8.4.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs-private/node-private/pull/204
description: Added `maxSettings` option with a default of 32.
- version:
- v13.3.0
- v12.16.0
@ -2037,6 +2040,8 @@ changes:
* `options` {Object}
* `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
for deflating header fields. **Default:** `4Kib`.
* `maxSettings` {number} Sets the maximum number of settings entries per
`SETTINGS` frame. The minimum value allowed is `1`. **Default:** `32`.
* `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
is permitted to use. The value is expressed in terms of number of megabytes,
e.g. `1` equal 1 megabyte. The minimum value allowed is `1`.
@ -2132,6 +2137,9 @@ server.listen(80);
<!-- YAML
added: v8.4.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs-private/node-private/pull/204
description: Added `maxSettings` option with a default of 32.
- version:
- v13.3.0
- v12.16.0
@ -2168,6 +2176,8 @@ changes:
**Default:** `false`.
* `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
for deflating header fields. **Default:** `4Kib`.
* `maxSettings` {number} Sets the maximum number of settings entries per
`SETTINGS` frame. The minimum value allowed is `1`. **Default:** `32`.
* `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
is permitted to use. The value is expressed in terms of number of megabytes,
e.g. `1` equal 1 megabyte. The minimum value allowed is `1`. This is a
@ -2250,6 +2260,9 @@ server.listen(80);
<!-- YAML
added: v8.4.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs-private/node-private/pull/204
description: Added `maxSettings` option with a default of 32.
- version: v13.0.0
pr-url: https://github.com/nodejs/node/pull/29144
description: The `PADDING_STRATEGY_CALLBACK` has been made equivalent to
@ -2273,6 +2286,8 @@ changes:
* `options` {Object}
* `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
for deflating header fields. **Default:** `4Kib`.
* `maxSettings` {number} Sets the maximum number of settings entries per
`SETTINGS` frame. The minimum value allowed is `1`. **Default:** `32`.
* `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
is permitted to use. The value is expressed in terms of number of megabytes,
e.g. `1` equal 1 megabyte. The minimum value allowed is `1`.

View File

@ -203,7 +203,8 @@ const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5;
const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6;
const IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS = 7;
const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
const IDX_OPTIONS_FLAGS = 9;
const IDX_OPTIONS_MAX_SETTINGS = 9;
const IDX_OPTIONS_FLAGS = 10;
function updateOptionsBuffer(options) {
let flags = 0;
@ -252,6 +253,11 @@ function updateOptionsBuffer(options) {
optionsBuffer[IDX_OPTIONS_MAX_SESSION_MEMORY] =
MathMax(1, options.maxSessionMemory);
}
if (typeof options.maxSettings === 'number') {
flags |= (1 << IDX_OPTIONS_MAX_SETTINGS);
optionsBuffer[IDX_OPTIONS_MAX_SETTINGS] =
MathMax(1, options.maxSettings);
}
optionsBuffer[IDX_OPTIONS_FLAGS] = flags;
}

View File

@ -193,6 +193,12 @@ Http2Options::Http2Options(Http2State* http2_state, SessionType type) {
// terms of MB increments (i.e. the value 1 == 1 MB)
if (flags & (1 << IDX_OPTIONS_MAX_SESSION_MEMORY))
set_max_session_memory(buffer[IDX_OPTIONS_MAX_SESSION_MEMORY] * 1000000);
if (flags & (1 << IDX_OPTIONS_MAX_SETTINGS)) {
nghttp2_option_set_max_settings(
option,
static_cast<size_t>(buffer[IDX_OPTIONS_MAX_SETTINGS]));
}
}
#define GRABSETTING(entries, count, name) \

View File

@ -54,6 +54,7 @@ namespace http2 {
IDX_OPTIONS_MAX_OUTSTANDING_PINGS,
IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS,
IDX_OPTIONS_MAX_SESSION_MEMORY,
IDX_OPTIONS_MAX_SETTINGS,
IDX_OPTIONS_FLAGS
};

View File

@ -0,0 +1,35 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const http2 = require('http2');
const server = http2.createServer({ maxSettings: 1 });
// TODO(@jasnell): There is still a session event
// emitted on the server side but it will be destroyed
// immediately after creation and there will be no
// stream created.
server.on('session', common.mustCall((session) => {
session.on('stream', common.mustNotCall());
session.on('remoteSettings', common.mustNotCall());
}));
server.on('stream', common.mustNotCall());
server.listen(0, common.mustCall(() => {
// Specify two settings entries when a max of 1 is allowed.
// Connection should error immediately.
const client = http2.connect(
`http://localhost:${server.address().port}`, {
settings: {
// The actual settings values do not matter.
headerTableSize: 1000,
enablePush: false,
} });
client.on('error', common.mustCall(() => {
server.close();
}));
}));

View File

@ -22,7 +22,8 @@ const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5;
const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6;
const IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS = 7;
const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
const IDX_OPTIONS_FLAGS = 9;
const IDX_OPTIONS_MAX_SETTINGS = 9;
const IDX_OPTIONS_FLAGS = 10;
{
updateOptionsBuffer({
@ -34,7 +35,8 @@ const IDX_OPTIONS_FLAGS = 9;
maxHeaderListPairs: 6,
maxOutstandingPings: 7,
maxOutstandingSettings: 8,
maxSessionMemory: 9
maxSessionMemory: 9,
maxSettings: 10,
});
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1);
@ -46,6 +48,7 @@ const IDX_OPTIONS_FLAGS = 9;
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS], 7);
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS], 8);
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SESSION_MEMORY], 9);
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SETTINGS], 10);
const flags = optionsBuffer[IDX_OPTIONS_FLAGS];
@ -57,6 +60,7 @@ const IDX_OPTIONS_FLAGS = 9;
ok(flags & (1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS));
ok(flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS));
ok(flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS));
ok(flags & (1 << IDX_OPTIONS_MAX_SETTINGS));
}
{