344 lines
9.9 KiB
Diff
344 lines
9.9 KiB
Diff
|
Ported from:
|
||
|
|
||
|
From 618eebdd175b598a06bbc4d3d1efeb85e3fa1429 Mon Sep 17 00:00:00 2001
|
||
|
From: Matteo Collina <hello@matteocollina.com>
|
||
|
Date: Thu, 23 Aug 2018 16:46:07 +0200
|
||
|
Subject: [PATCH] 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: https://github.com/nodejs-private/node-private/pull/152
|
||
|
Ref: https://github.com/nodejs-private/node-private/pull/144
|
||
|
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
|
||
|
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
|
||
|
Reviewed-By: James M Snell <jasnell@gmail.com>
|
||
|
|
||
|
|
||
|
Index: node-v4.9.1/doc/api/http.md
|
||
|
===================================================================
|
||
|
--- node-v4.9.1.orig/doc/api/http.md
|
||
|
+++ node-v4.9.1/doc/api/http.md
|
||
|
@@ -704,6 +704,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
|
||
|
Index: node-v4.9.1/doc/api/https.md
|
||
|
===================================================================
|
||
|
--- node-v4.9.1.orig/doc/api/https.md
|
||
|
+++ node-v4.9.1/doc/api/https.md
|
||
|
@@ -21,6 +21,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
|
||
|
@@ -257,6 +263,7 @@ var 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
|
||
|
Index: node-v4.9.1/lib/_http_outgoing.js
|
||
|
===================================================================
|
||
|
--- node-v4.9.1.orig/lib/_http_outgoing.js
|
||
|
+++ node-v4.9.1/lib/_http_outgoing.js
|
||
|
@@ -31,20 +31,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);
|
||
|
Index: node-v4.9.1/lib/_http_server.js
|
||
|
===================================================================
|
||
|
--- node-v4.9.1.orig/lib/_http_server.js
|
||
|
+++ node-v4.9.1/lib/_http_server.js
|
||
|
@@ -14,6 +14,7 @@ const continueExpression = common.contin
|
||
|
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',
|
||
|
@@ -251,6 +252,7 @@ function Server(requestListener) {
|
||
|
this.timeout = 2 * 60 * 1000;
|
||
|
|
||
|
this._pendingResponseData = 0;
|
||
|
+ this.headersTimeout = 40 * 1000; // 40 seconds
|
||
|
}
|
||
|
util.inherits(Server, net.Server);
|
||
|
|
||
|
@@ -324,6 +326,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;
|
||
|
|
||
|
@@ -376,6 +381,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);
|
||
|
}
|
||
|
|
||
|
@@ -444,7 +463,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.
|
||
|
Index: node-v4.9.1/lib/https.js
|
||
|
===================================================================
|
||
|
--- node-v4.9.1.orig/lib/https.js
|
||
|
+++ node-v4.9.1/lib/https.js
|
||
|
@@ -34,6 +34,8 @@ function Server(opts, requestListener) {
|
||
|
});
|
||
|
|
||
|
this.timeout = 2 * 60 * 1000;
|
||
|
+
|
||
|
+ this.headersTimeout = 40 * 1000; // 40 seconds
|
||
|
}
|
||
|
inherits(Server, tls.Server);
|
||
|
exports.Server = Server;
|
||
|
Index: node-v4.9.1/test/parallel/test-http-slow-headers.js
|
||
|
===================================================================
|
||
|
--- /dev/null
|
||
|
+++ node-v4.9.1/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').createServer;
|
||
|
+const connect = require('net').connect;
|
||
|
+
|
||
|
+// 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);
|
||
|
+}));
|
||
|
Index: node-v4.9.1/test/parallel/test-https-slow-headers.js
|
||
|
===================================================================
|
||
|
--- /dev/null
|
||
|
+++ node-v4.9.1/test/parallel/test-https-slow-headers.js
|
||
|
@@ -0,0 +1,80 @@
|
||
|
+'use strict';
|
||
|
+
|
||
|
+const common = require('../common');
|
||
|
+const path = require('path');
|
||
|
+const fs = require('fs');
|
||
|
+
|
||
|
+const fixturesDir = path.join(__dirname, '..', 'fixtures');
|
||
|
+
|
||
|
+function fixturesPath(p1, p2) {
|
||
|
+ return path.join(fixturesDir, p1, p2);
|
||
|
+}
|
||
|
+
|
||
|
+function readKey(name, enc) {
|
||
|
+ return fs.readFileSync(fixturesPath('keys', name), enc);
|
||
|
+}
|
||
|
+
|
||
|
+if (!common.hasCrypto)
|
||
|
+ common.skip('missing crypto');
|
||
|
+
|
||
|
+const assert = require('assert');
|
||
|
+const createServer = require('https').createServer;
|
||
|
+const connect = require('tls').connect;
|
||
|
+
|
||
|
+// 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);
|
||
|
+}));
|