Files
nodejs4/CVE-2018-12122.patch
Adam Majer 57718cd79b * cli: add --max-http-header-size flag (max_header_size.patch)
+ 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
2019-01-09 14:07:18 +00:00

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);
+}));