Skip to content

Commit 696f063

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#151 Ref: 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 93dba83 commit 696f063

File tree

8 files changed

+199
-12
lines changed

8 files changed

+199
-12
lines changed

doc/api/http.md

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

883883
Limits maximum incoming headers count. If set to 0 - no limit will be applied.
884884

885+
### server.headersTimeout
886+
<!-- YAML
887+
added: REPLACEME
888+
-->
889+
890+
* {number} **Default:** `40000`
891+
892+
Limit the amount of time the parser will wait to receive the complete HTTP
893+
headers.
894+
895+
In case of inactivity, the rules defined in [server.timeout][] apply. However,
896+
that inactivity based timeout would still allow the connection to be kept open
897+
if the headers are being sent very slowly (by default, up to a byte per 2
898+
minutes). In order to prevent this, whenever header data arrives an additional
899+
check is made that more than `server.headersTimeout` milliseconds has not
900+
passed since the connection was established. If the check fails, a `'timeout'`
901+
event is emitted on the server object, and (by default) the socket is destroyed.
902+
See [server.timeout][] for more information on how timeout behaviour can be
903+
customised.
904+
885905
### server.setTimeout([msecs][, callback])
886906
<!-- YAML
887907
added: v0.9.12

doc/api/https.md

+7
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ See [`server.close()`][`http.close()`] from the HTTP module for details.
3636
Starts the HTTPS server listening for encrypted connections.
3737
This method is identical to [`server.listen()`][] from [`net.Server`][].
3838

39+
### server.headersTimeout
40+
41+
- {number} **Default:** `40000`
42+
43+
See [`http.Server#headersTimeout`][].
44+
3945
### server.setTimeout([msecs][, callback])
4046
<!-- YAML
4147
added: v0.11.2
@@ -253,6 +259,7 @@ const req = https.request(options, (res) => {
253259
[`URL`]: url.html#url_the_whatwg_url_api
254260
[`http.Agent`]: http.html#http_class_http_agent
255261
[`http.Server#keepAliveTimeout`]: http.html#http_server_keepalivetimeout
262+
[`http.Server#headersTimeout`]: http.html#http_server_headerstimeout
256263
[`http.Server#setTimeout()`]: http.html#http_server_settimeout_msecs_callback
257264
[`http.Server#timeout`]: http.html#http_server_timeout
258265
[`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
@@ -296,6 +296,7 @@ function Server(options, requestListener) {
296296
this.keepAliveTimeout = 5000;
297297
this._pendingResponseData = 0;
298298
this.maxHeadersCount = null;
299+
this.headersTimeout = 40 * 1000; // 40 seconds
299300
}
300301
util.inherits(Server, net.Server);
301302

@@ -334,6 +335,9 @@ function connectionListenerInternal(server, socket) {
334335
var parser = parsers.alloc();
335336
parser.reinitialize(HTTPParser.REQUEST, parser[is_reused_symbol]);
336337
parser.socket = socket;
338+
339+
// We are starting to wait for our headers.
340+
parser.parsingHeadersStart = nowDate();
337341
socket.parser = parser;
338342
parser.incoming = null;
339343

@@ -475,7 +479,20 @@ function socketOnData(server, socket, parser, state, d) {
475479

476480
function onParserExecute(server, socket, parser, state, ret) {
477481
socket._unrefTimer();
482+
const start = parser.parsingHeadersStart;
478483
debug('SERVER socketOnParserExecute %d', ret);
484+
485+
// If we have not parsed the headers, destroy the socket
486+
// after server.headersTimeout to protect from DoS attacks.
487+
// start === 0 means that we have parsed headers.
488+
if (start !== 0 && nowDate() - start > server.headersTimeout) {
489+
const serverTimeout = server.emit('timeout', socket);
490+
491+
if (!serverTimeout)
492+
socket.destroy();
493+
return;
494+
}
495+
479496
onParserExecuteCommon(server, socket, parser, state, ret, undefined);
480497
}
481498

@@ -579,6 +596,9 @@ function resOnFinish(req, res, socket, state, server) {
579596
function parserOnIncoming(server, socket, state, req, keepAlive) {
580597
resetSocketTimeout(server, socket, state);
581598

599+
// Set to zero to communicate that we have finished parsing.
600+
socket.parser.parsingHeadersStart = 0;
601+
582602
state.incoming.push(req);
583603

584604
// If the writable end isn't consuming, then stop reading

lib/https.js

+2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ function Server(opts, requestListener) {
7070

7171
this.timeout = 2 * 60 * 1000;
7272
this.keepAliveTimeout = 5000;
73+
74+
this.headersTimeout = 40 * 1000; // 40 seconds
7375
}
7476
inherits(Server, tls.Server);
7577
exports.Server = Server;

lib/internal/http.js

+23-10
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,30 @@
22

33
const timers = require('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();
10-
timers.enroll(utcDate, 1000 - d.getMilliseconds());
11-
timers._unrefActive(utcDate);
12-
}
13-
return dateCache;
14+
if (!utcCache) cache();
15+
return utcCache;
1416
}
15-
utcDate._onTimeout = function() {
16-
dateCache = undefined;
17+
18+
function cache() {
19+
const d = new Date();
20+
nowCache = d.valueOf();
21+
utcCache = d.toUTCString();
22+
timers.enroll(cache, 1000 - d.getMilliseconds());
23+
timers._unrefActive(cache);
24+
}
25+
26+
cache._onTimeout = function() {
27+
nowCache = undefined;
28+
utcCache = undefined;
1729
};
1830

1931
function ondrain() {
@@ -23,5 +35,6 @@ function ondrain() {
2335
module.exports = {
2436
outHeadersKey: Symbol('outHeadersKey'),
2537
ondrain,
38+
nowDate,
2639
utcDate
2740
};

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

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

0 commit comments

Comments
 (0)