+ add maxHeaderSize property (max_header_size.patch) (CVE-2018-12121.patch - CVE-2018-12121, bsc#1117626) + A timeout of 40 seconds now applies to servers receiving HTTP headers. This value can be adjusted with server.headersTimeout. Where headers are not completely received within this period, the socket is destroyed on the next received chunk. In conjunction with server.setTimeout(), this aids in protecting against excessive resource retention and possible Denial of Service. (CVE-2018-12122.patch - CVE-2018-12122, bsc#1117627) (CVE-2018-12116.patch - CVE-2018-12116, bsc#1117630) (CVE-2018-12123.patch - CVE-2018-12123, bnc#1117629) OBS-URL: https://build.opensuse.org/package/show/devel:languages:nodejs/nodejs4?expand=0&rev=101
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);
|
|
+}));
|