Skip to content

Commit 315ee2e

Browse files
mcollinarvagg
authored andcommitted
http,https: protect against slow headers attack
CVE-2018-12122 An attacker can send a char/s within headers and exahust the resources (file descriptors) of a system even with a tight max header length protection. This PR destroys a socket if it has not received the headers in 40s. PR-URL: nodejs-private/node-private#144 Reviewed-By: Sam Roberts <vieuxtech@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 4ecbd3b commit 315ee2e

File tree

8 files changed

+182
-10
lines changed

8 files changed

+182
-10
lines changed

doc/api/http.md

+20
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,26 @@ added: v0.7.0
958958

959959
Limits maximum incoming headers count. If set to 0, no limit will be applied.
960960

961+
### server.headersTimeout
962+
<!-- YAML
963+
added: REPLACEME
964+
-->
965+
966+
* {number} **Default:** `40000`
967+
968+
Limit the amount of time the parser will wait to receive the complete HTTP
969+
headers.
970+
971+
In case of inactivity, the rules defined in [server.timeout][] apply. However,
972+
that inactivity based timeout would still allow the connection to be kept open
973+
if the headers are being sent very slowly (by default, up to a byte per 2
974+
minutes). In order to prevent this, whenever header data arrives an additional
975+
check is made that more than `server.headersTimeout` milliseconds has not
976+
passed since the connection was established. If the check fails, a `'timeout'`
977+
event is emitted on the server object, and (by default) the socket is destroyed.
978+
See [server.timeout][] for more information on how timeout behaviour can be
979+
customised.
980+
961981
### server.setTimeout([msecs][, callback])
962982
<!-- YAML
963983
added: v0.9.12

doc/api/https.md

+7
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ This method is identical to [`server.listen()`][] from [`net.Server`][].
4444

4545
See [`http.Server#maxHeadersCount`][].
4646

47+
### server.headersTimeout
48+
49+
- {number} **Default:** `40000`
50+
51+
See [`http.Server#headersTimeout`][].
52+
4753
### server.setTimeout([msecs][, callback])
4854
<!-- YAML
4955
added: v0.11.2
@@ -363,6 +369,7 @@ headers: max-age=0; pin-sha256="WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18="; p
363369
[`http.Agent`]: http.html#http_class_http_agent
364370
[`http.Server#keepAliveTimeout`]: http.html#http_server_keepalivetimeout
365371
[`http.Server#maxHeadersCount`]: http.html#http_server_maxheaderscount
372+
[`http.Server#headersTimeout`]: http.html#http_server_headerstimeout
366373
[`http.Server#setTimeout()`]: http.html#http_server_settimeout_msecs_callback
367374
[`http.Server#timeout`]: http.html#http_server_timeout
368375
[`http.Server`]: http.html#http_class_http_server

lib/_http_server.js

+21-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const {
3737
_checkInvalidHeaderChar: checkInvalidHeaderChar
3838
} = require('_http_common');
3939
const { OutgoingMessage } = require('_http_outgoing');
40-
const { outHeadersKey, ondrain } = require('internal/http');
40+
const { outHeadersKey, ondrain, nowDate } = require('internal/http');
4141
const {
4242
defaultTriggerAsyncIdScope,
4343
getOrSetAsyncId
@@ -303,6 +303,7 @@ function Server(options, requestListener) {
303303
this.keepAliveTimeout = 5000;
304304
this._pendingResponseData = 0;
305305
this.maxHeadersCount = null;
306+
this.headersTimeout = 40 * 1000; // 40 seconds
306307
}
307308
util.inherits(Server, net.Server);
308309

@@ -341,6 +342,9 @@ function connectionListenerInternal(server, socket) {
341342
var parser = parsers.alloc();
342343
parser.reinitialize(HTTPParser.REQUEST, parser[is_reused_symbol]);
343344
parser.socket = socket;
345+
346+
// We are starting to wait for our headers.
347+
parser.parsingHeadersStart = nowDate();
344348
socket.parser = parser;
345349

346350
// Propagate headers limit from server instance to parser
@@ -478,7 +482,20 @@ function socketOnData(server, socket, parser, state, d) {
478482

479483
function onParserExecute(server, socket, parser, state, ret) {
480484
socket._unrefTimer();
485+
const start = parser.parsingHeadersStart;
481486
debug('SERVER socketOnParserExecute %d', ret);
487+
488+
// If we have not parsed the headers, destroy the socket
489+
// after server.headersTimeout to protect from DoS attacks.
490+
// start === 0 means that we have parsed headers.
491+
if (start !== 0 && nowDate() - start > server.headersTimeout) {
492+
const serverTimeout = server.emit('timeout', socket);
493+
494+
if (!serverTimeout)
495+
socket.destroy();
496+
return;
497+
}
498+
482499
onParserExecuteCommon(server, socket, parser, state, ret, undefined);
483500
}
484501

@@ -595,6 +612,9 @@ function emitCloseNT(self) {
595612
function parserOnIncoming(server, socket, state, req, keepAlive) {
596613
resetSocketTimeout(server, socket, state);
597614

615+
// Set to zero to communicate that we have finished parsing.
616+
socket.parser.parsingHeadersStart = 0;
617+
598618
if (req.upgrade) {
599619
req.upgrade = req.method === 'CONNECT' ||
600620
server.listenerCount('upgrade') > 0;

lib/https.js

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ function Server(opts, requestListener) {
7474
this.timeout = 2 * 60 * 1000;
7575
this.keepAliveTimeout = 5000;
7676
this.maxHeadersCount = null;
77+
this.headersTimeout = 40 * 1000; // 40 seconds
7778
}
7879
inherits(Server, tls.Server);
7980

lib/internal/http.js

+19-8
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,29 @@
22

33
const { setUnrefTimeout } = require('internal/timers');
44

5-
var dateCache;
5+
var nowCache;
6+
var utcCache;
7+
8+
function nowDate() {
9+
if (!nowCache) cache();
10+
return nowCache;
11+
}
12+
613
function utcDate() {
7-
if (!dateCache) {
8-
const d = new Date();
9-
dateCache = d.toUTCString();
14+
if (!utcCache) cache();
15+
return utcCache;
16+
}
1017

11-
setUnrefTimeout(resetCache, 1000 - d.getMilliseconds());
12-
}
13-
return dateCache;
18+
function cache() {
19+
const d = new Date();
20+
nowCache = d.valueOf();
21+
utcCache = d.toUTCString();
22+
setUnrefTimeout(resetCache, 1000 - d.getMilliseconds());
1423
}
1524

1625
function resetCache() {
17-
dateCache = undefined;
26+
nowCache = undefined;
27+
utcCache = undefined;
1828
}
1929

2030
function ondrain() {
@@ -24,5 +34,6 @@ function ondrain() {
2434
module.exports = {
2535
outHeadersKey: Symbol('outHeadersKey'),
2636
ondrain,
37+
nowDate,
2738
utcDate
2839
};

test/async-hooks/test-graph.http.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ process.on('exit', function() {
4545
triggerAsyncId: 'tcp:2' },
4646
{ type: 'Timeout',
4747
id: 'timeout:2',
48-
triggerAsyncId: 'httpparser:2' },
48+
triggerAsyncId: 'tcp:2' },
4949
{ type: 'SHUTDOWNWRAP',
5050
id: 'shutdown:1',
5151
triggerAsyncId: 'tcp:2' } ]
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { createServer } = require('http');
6+
const { connect } = require('net');
7+
const { finished } = require('stream');
8+
9+
// This test validates that the 'timeout' event fires
10+
// after server.headersTimeout.
11+
12+
const headers =
13+
'GET / HTTP/1.1\r\n' +
14+
'Host: localhost\r\n' +
15+
'Agent: node\r\n';
16+
17+
const server = createServer(common.mustNotCall());
18+
let sendCharEvery = 1000;
19+
20+
// 40 seconds is the default
21+
assert.strictEqual(server.headersTimeout, 40 * 1000);
22+
23+
// Pass a REAL env variable to shortening up the default
24+
// value which is 40s otherwise this is useful for manual
25+
// testing
26+
if (!process.env.REAL) {
27+
sendCharEvery = common.platformTimeout(10);
28+
server.headersTimeout = 2 * sendCharEvery;
29+
}
30+
31+
server.once('timeout', common.mustCall((socket) => {
32+
socket.destroy();
33+
}));
34+
35+
server.listen(0, common.mustCall(() => {
36+
const client = connect(server.address().port);
37+
client.write(headers);
38+
client.write('X-CRASH: ');
39+
40+
const interval = setInterval(() => {
41+
client.write('a');
42+
}, sendCharEvery);
43+
44+
client.resume();
45+
46+
finished(client, common.mustCall((err) => {
47+
clearInterval(interval);
48+
server.close();
49+
}));
50+
}));
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { readKey } = require('../common/fixtures');
5+
6+
if (!common.hasCrypto)
7+
common.skip('missing crypto');
8+
9+
const assert = require('assert');
10+
const { createServer } = require('https');
11+
const { connect } = require('tls');
12+
const { finished } = require('stream');
13+
14+
// This test validates that the 'timeout' event fires
15+
// after server.headersTimeout.
16+
17+
const headers =
18+
'GET / HTTP/1.1\r\n' +
19+
'Host: localhost\r\n' +
20+
'Agent: node\r\n';
21+
22+
const server = createServer({
23+
key: readKey('agent1-key.pem'),
24+
cert: readKey('agent1-cert.pem'),
25+
ca: readKey('ca1-cert.pem'),
26+
}, common.mustNotCall());
27+
28+
let sendCharEvery = 1000;
29+
30+
// 40 seconds is the default
31+
assert.strictEqual(server.headersTimeout, 40 * 1000);
32+
33+
// pass a REAL env variable to shortening up the default
34+
// value which is 40s otherwise
35+
// this is useful for manual testing
36+
if (!process.env.REAL) {
37+
sendCharEvery = common.platformTimeout(10);
38+
server.headersTimeout = 2 * sendCharEvery;
39+
}
40+
41+
server.once('timeout', common.mustCall((socket) => {
42+
socket.destroy();
43+
}));
44+
45+
server.listen(0, common.mustCall(() => {
46+
const client = connect({
47+
port: server.address().port,
48+
rejectUnauthorized: false
49+
});
50+
client.write(headers);
51+
client.write('X-CRASH: ');
52+
53+
const interval = setInterval(() => {
54+
client.write('a');
55+
}, sendCharEvery);
56+
57+
client.resume();
58+
59+
finished(client, common.mustCall((err) => {
60+
clearInterval(interval);
61+
server.close();
62+
}));
63+
}));

0 commit comments

Comments
 (0)