From ee618a7ab239c98d945c723a4e225bc409151736 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/144
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
---
 doc/api/http.md                          | 20 ++++++++
 doc/api/https.md                         |  7 +++
 lib/_http_server.js                      | 22 ++++++++-
 lib/https.js                             |  1 +
 lib/internal/http.js                     | 27 +++++++---
 test/async-hooks/test-graph.http.js      |  2 +-
 test/parallel/test-http-slow-headers.js  | 50 +++++++++++++++++++
 test/parallel/test-https-slow-headers.js | 63 ++++++++++++++++++++++++
 8 files changed, 182 insertions(+), 10 deletions(-)
 create mode 100644 test/parallel/test-http-slow-headers.js
 create mode 100644 test/parallel/test-https-slow-headers.js

diff --git a/doc/api/http.md b/doc/api/http.md
index 1c6b5717e004..13373debb404 100644
--- a/doc/api/http.md
+++ b/doc/api/http.md
@@ -958,6 +958,26 @@ added: v0.7.0
 
 Limits maximum incoming headers count. If set to 0, no limit will be applied.
 
+### 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.setTimeout([msecs][, callback])
 <!-- YAML
 added: v0.9.12
diff --git a/doc/api/https.md b/doc/api/https.md
index 777fbab741c8..81a5bcce934e 100644
--- a/doc/api/https.md
+++ b/doc/api/https.md
@@ -44,6 +44,12 @@ This method is identical to [`server.listen()`][] from [`net.Server`][].
 
 See [`http.Server#maxHeadersCount`][].
 
+### server.headersTimeout
+
+- {number} **Default:** `40000`
+
+See [`http.Server#headersTimeout`][].
+
 ### server.setTimeout([msecs][, callback])
 <!-- YAML
 added: v0.11.2
@@ -363,6 +369,7 @@ headers: max-age=0; pin-sha256="WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18="; p
 [`http.Agent`]: http.html#http_class_http_agent
 [`http.Server#keepAliveTimeout`]: http.html#http_server_keepalivetimeout
 [`http.Server#maxHeadersCount`]: http.html#http_server_maxheaderscount
+[`http.Server#headersTimeout`]: http.html#http_server_headerstimeout
 [`http.Server#setTimeout()`]: http.html#http_server_settimeout_msecs_callback
 [`http.Server#timeout`]: http.html#http_server_timeout
 [`http.Server`]: http.html#http_class_http_server
diff --git a/lib/_http_server.js b/lib/_http_server.js
index 96f05f5819d3..c171b1d3e78a 100644
--- a/lib/_http_server.js
+++ b/lib/_http_server.js
@@ -37,7 +37,7 @@ const {
   _checkInvalidHeaderChar: checkInvalidHeaderChar
 } = require('_http_common');
 const { OutgoingMessage } = require('_http_outgoing');
-const { outHeadersKey, ondrain } = require('internal/http');
+const { outHeadersKey, ondrain, nowDate } = require('internal/http');
 const {
   defaultTriggerAsyncIdScope,
   getOrSetAsyncId
@@ -306,6 +306,7 @@ function Server(options, requestListener) {
   this.keepAliveTimeout = 5000;
   this._pendingResponseData = 0;
   this.maxHeadersCount = null;
+  this.headersTimeout = 40 * 1000; // 40 seconds
 }
 util.inherits(Server, net.Server);
 
@@ -344,6 +345,9 @@ function connectionListenerInternal(server, 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;
 
   // Propagate headers limit from server instance to parser
@@ -481,7 +485,20 @@ function socketOnData(server, socket, parser, state, d) {
 
 function onParserExecute(server, socket, parser, state, ret) {
   socket._unrefTimer();
+  const start = parser.parsingHeadersStart;
   debug('SERVER socketOnParserExecute %d', ret);
+
+  // 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 > server.headersTimeout) {
+    const serverTimeout = server.emit('timeout', socket);
+
+    if (!serverTimeout)
+      socket.destroy();
+    return;
+  }
+
   onParserExecuteCommon(server, socket, parser, state, ret, undefined);
 }
 
@@ -598,6 +615,9 @@ function emitCloseNT(self) {
 function parserOnIncoming(server, socket, state, req, keepAlive) {
   resetSocketTimeout(server, socket, state);
 
+  // Set to zero to communicate that we have finished parsing.
+  socket.parser.parsingHeadersStart = 0;
+
   if (req.upgrade) {
     req.upgrade = req.method === 'CONNECT' ||
                   server.listenerCount('upgrade') > 0;
diff --git a/lib/https.js b/lib/https.js
index 66e76c1f0509..0854c3d44057 100644
--- a/lib/https.js
+++ b/lib/https.js
@@ -74,6 +74,7 @@ function Server(opts, requestListener) {
   this.timeout = 2 * 60 * 1000;
   this.keepAliveTimeout = 5000;
   this.maxHeadersCount = null;
+  this.headersTimeout = 40 * 1000; // 40 seconds
 }
 inherits(Server, tls.Server);
 
diff --git a/lib/internal/http.js b/lib/internal/http.js
index 2b9c948aeefb..47a51fb73949 100644
--- a/lib/internal/http.js
+++ b/lib/internal/http.js
@@ -2,19 +2,29 @@
 
 const { setUnrefTimeout } = require('internal/timers');
 
-var dateCache;
+var nowCache;
+var utcCache;
+
+function nowDate() {
+  if (!nowCache) cache();
+  return nowCache;
+}
+
 function utcDate() {
-  if (!dateCache) {
-    const d = new Date();
-    dateCache = d.toUTCString();
+  if (!utcCache) cache();
+  return utcCache;
+}
 
-    setUnrefTimeout(resetCache, 1000 - d.getMilliseconds());
-  }
-  return dateCache;
+function cache() {
+  const d = new Date();
+  nowCache = d.valueOf();
+  utcCache = d.toUTCString();
+  setUnrefTimeout(resetCache, 1000 - d.getMilliseconds());
 }
 
 function resetCache() {
-  dateCache = undefined;
+  nowCache = undefined;
+  utcCache = undefined;
 }
 
 function ondrain() {
@@ -24,5 +34,6 @@ function ondrain() {
 module.exports = {
   outHeadersKey: Symbol('outHeadersKey'),
   ondrain,
+  nowDate,
   utcDate
 };
