Skip to content

Commit

Permalink
http,https: protect against slow headers attack
Browse files Browse the repository at this point in the history
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#152
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>
  • Loading branch information
mcollina authored and rvagg committed Nov 27, 2018
1 parent 92231a5 commit 618eebd
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 11 deletions.
20 changes: 20 additions & 0 deletions doc/api/http.md
Expand Up @@ -848,6 +848,26 @@ for handling socket timeouts.

Returns `server`.

### server.headersTimeout
<!-- YAML
added: REPLACEME
-->

* {number} **Default:** `40000`

Limit the amount of time the parser will wait to receive the complete HTTP
headers.

In case of inactivity, the rules defined in [server.timeout][] apply. However,
that inactivity based timeout would still allow the connection to be kept open
if the headers are being sent very slowly (by default, up to a byte per 2
minutes). In order to prevent this, whenever header data arrives an additional
check is made that more than `server.headersTimeout` milliseconds has not
passed since the connection was established. If the check fails, a `'timeout'`
event is emitted on the server object, and (by default) the socket is destroyed.
See [server.timeout][] for more information on how timeout behaviour can be
customised.

### server.timeout
<!-- YAML
added: v0.9.12
Expand Down
7 changes: 7 additions & 0 deletions doc/api/https.md
Expand Up @@ -23,6 +23,12 @@ added: v0.3.4
This class is a subclass of `tls.Server` and emits events same as
[`http.Server`][]. See [`http.Server`][] for more information.

### server.headersTimeout

- {number} **Default:** `40000`

See [`http.Server#headersTimeout`][].

### server.setTimeout([msecs][, callback])
<!-- YAML
added: v0.11.2
Expand Down Expand Up @@ -233,6 +239,7 @@ const req = https.request(options, (res) => {
[`Buffer`]: buffer.html#buffer_buffer
[`globalAgent`]: #https_https_globalagent
[`http.Agent`]: http.html#http_class_http_agent
[`http.Server#headersTimeout`]: http.html#http_server_headerstimeout
[`http.close()`]: http.html#http_server_close_callback
[`http.get()`]: http.html#http_http_get_options_callback
[`http.listen()`]: http.html#http_server_listen_port_hostname_backlog_callback
Expand Down
33 changes: 23 additions & 10 deletions lib/_http_outgoing.js
Expand Up @@ -30,20 +30,33 @@ const automaticHeaders = {
};


var dateCache;
var nowCache;
var utcCache;

function nowDate() {
if (!nowCache) cache();
return nowCache;
}

function utcDate() {
if (!dateCache) {
var d = new Date();
dateCache = d.toUTCString();
timers.enroll(utcDate, 1000 - d.getMilliseconds());
timers._unrefActive(utcDate);
}
return dateCache;
if (!utcCache) cache();
return utcCache;
}
utcDate._onTimeout = function() {
dateCache = undefined;

function cache() {
const d = new Date();
nowCache = d.valueOf();
utcCache = d.toUTCString();
timers.enroll(cache, 1000 - d.getMilliseconds());
timers._unrefActive(cache);
}

cache._onTimeout = function() {
nowCache = undefined;
utcCache = undefined;
};

exports.nowDate = nowDate;

function OutgoingMessage() {
Stream.call(this);
Expand Down
20 changes: 19 additions & 1 deletion lib/_http_server.js
Expand Up @@ -13,6 +13,7 @@ const continueExpression = common.continueExpression;
const chunkExpression = common.chunkExpression;
const httpSocketSetup = common.httpSocketSetup;
const OutgoingMessage = require('_http_outgoing').OutgoingMessage;
const nowDate = require('_http_outgoing').nowDate;

const STATUS_CODES = exports.STATUS_CODES = {
100: 'Continue',
Expand Down Expand Up @@ -244,6 +245,7 @@ function Server(requestListener) {
this.timeout = 2 * 60 * 1000;

this._pendingResponseData = 0;
this.headersTimeout = 40 * 1000; // 40 seconds
}
util.inherits(Server, net.Server);

Expand Down Expand Up @@ -317,6 +319,9 @@ function connectionListener(socket) {
var parser = parsers.alloc();
parser.reinitialize(HTTPParser.REQUEST);
parser.socket = socket;

// We are starting to wait for our headers.
parser.parsingHeadersStart = nowDate();
socket.parser = parser;
parser.incoming = null;

Expand Down Expand Up @@ -374,6 +379,20 @@ function connectionListener(socket) {
function onParserExecute(ret, d) {
socket._unrefTimer();
debug('SERVER socketOnParserExecute %d', ret);

var start = parser.parsingHeadersStart;

// If we have not parsed the headers, destroy the socket
// after server.headersTimeout to protect from DoS attacks.
// start === 0 means that we have parsed headers.
if (start !== 0 && nowDate() - start > self.headersTimeout) {
var serverTimeout = self.emit('timeout', socket);

if (!serverTimeout)
socket.destroy();
return;
}

onParserExecuteCommon(ret, undefined);
}

Expand Down Expand Up @@ -442,7 +461,6 @@ function connectionListener(socket) {
}
}


// The following callback is issued after the headers have been read on a
// new message. In this callback we setup the response object and pass it
// to the user.
Expand Down
2 changes: 2 additions & 0 deletions lib/https.js
Expand Up @@ -37,6 +37,8 @@ function Server(opts, requestListener) {
});

this.timeout = 2 * 60 * 1000;

this.headersTimeout = 40 * 1000; // 40 seconds
}
inherits(Server, tls.Server);
exports.Server = Server;
Expand Down
56 changes: 56 additions & 0 deletions test/parallel/test-http-slow-headers.js
@@ -0,0 +1,56 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const { createServer } = require('http');
const { connect } = require('net');

// This test validates that the 'timeout' event fires
// after server.headersTimeout.

const headers =
'GET / HTTP/1.1\r\n' +
'Host: localhost\r\n' +
'Agent: node\r\n';

const server = createServer(common.mustNotCall());
let sendCharEvery = 1000;

// 40 seconds is the default
assert.strictEqual(server.headersTimeout, 40 * 1000);

// Pass a REAL env variable to shortening up the default
// value which is 40s otherwise this is useful for manual
// testing
if (!process.env.REAL) {
sendCharEvery = common.platformTimeout(10);
server.headersTimeout = 2 * sendCharEvery;
}

server.once('timeout', common.mustCall((socket) => {
socket.destroy();
}));

server.listen(0, common.mustCall(() => {
const client = connect(server.address().port);
client.write(headers);
client.write('X-CRASH: ');

const interval = setInterval(() => {
client.write('a');
}, sendCharEvery);

client.resume();

const onClose = common.mustCall(() => {
client.removeListener('close', onClose);
client.removeListener('error', onClose);
client.removeListener('end', onClose);
clearInterval(interval);
server.close();
});

client.on('error', onClose);
client.on('close', onClose);
client.on('end', onClose);
}));
69 changes: 69 additions & 0 deletions test/parallel/test-https-slow-headers.js
@@ -0,0 +1,69 @@
'use strict';

const common = require('../common');
const { readKey } = require('../common/fixtures');

if (!common.hasCrypto)
common.skip('missing crypto');

const assert = require('assert');
const { createServer } = require('https');
const { connect } = require('tls');

// This test validates that the 'timeout' event fires
// after server.headersTimeout.

const headers =
'GET / HTTP/1.1\r\n' +
'Host: localhost\r\n' +
'Agent: node\r\n';

const server = createServer({
key: readKey('agent1-key.pem'),
cert: readKey('agent1-cert.pem'),
ca: readKey('ca1-cert.pem'),
}, common.mustNotCall());

let sendCharEvery = 1000;

// 40 seconds is the default
assert.strictEqual(server.headersTimeout, 40 * 1000);

// pass a REAL env variable to shortening up the default
// value which is 40s otherwise
// this is useful for manual testing
if (!process.env.REAL) {
sendCharEvery = common.platformTimeout(10);
server.headersTimeout = 2 * sendCharEvery;
}

server.once('timeout', common.mustCall((socket) => {
socket.destroy();
}));

server.listen(0, common.mustCall(() => {
const client = connect({
port: server.address().port,
rejectUnauthorized: false
});
client.write(headers);
client.write('X-CRASH: ');

const interval = setInterval(() => {
client.write('a');
}, sendCharEvery);

client.resume();

const onClose = common.mustCall(() => {
client.removeListener('close', onClose);
client.removeListener('error', onClose);
client.removeListener('end', onClose);
clearInterval(interval);
server.close();
});

client.on('error', onClose);
client.on('close', onClose);
client.on('end', onClose);
}));

0 comments on commit 618eebd

Please sign in to comment.