CVE-2016-1000232: Improper Input Validation (CWE-20)
value: NodeJS Tough-Cookie version 2.2.2 contains a Regular Expression Parsing vulnerability in HTTP request Cookie Header parsing that can result in Denial of Service. This attack appear to be exploitable via Custom HTTP header passed by client. This vulnerability appears to have been fixed in 2.3.0.
CWE description: The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
CVSS v3 base score: 5.3
Published: 2018-09-05T17:29Z
Repository: https://github.com/salesforce/tough-cookie
Language: JavaScript
Files changed: lib/cookie.js, test/parsing_test.js
Commit date: 2016-07-21 11:35:26 -0700
Commit message: Reduce parse time for many semicolons.

Bug Credit To: David Kirchner <dpk@stream.me>

Diff: diff --git a/lib/cookie.js b/lib/cookie.js
index 12da297..c3dacfe 100644
--- a/lib/cookie.js
+++ b/lib/cookie.js
@@ -66,13 +66,10 @@ var LOOSE_COOKIE_PAIR = /^((?:=)?([^=;]*)\s*=\s*)?([^\n\r\0]*)/;
 
 // RFC6265 S4.1.1 defines path value as 'any CHAR except CTLs or ";"'
 // Note ';' is \x3B
 var PATH_VALUE = /[\x20-\x3A\x3C-\x7E]+/;
 
-// Used for checking whether or not there is a trailing semi-colon
-var TRAILING_SEMICOLON = /;+$/;
-
 var DAY_OF_MONTH = /^(\d{1,2})[^\d]*$/;
 var TIME = /^(\d{1,2})[^\d]*:(\d{1,2})[^\d]*:(\d{1,2})[^\d]*$/;
 var MONTH = /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/i;
 
 var MONTH_TO_NUM = {
@@ -325,16 +322,10 @@ function parse(str, options) {
   if (!options || typeof options !== 'object') {
     options = {};
   }
   str = str.trim();
 
-  // S4.1.1 Trailing semi-colons are not part of the specification.
-  var semiColonCheck = TRAILING_SEMICOLON.exec(str);
-  if (semiColonCheck) {
-    str = str.slice(0, semiColonCheck.index);
-  }
-
   // We use a regex to parse the "name-value-pair" part of S5.2
   var firstSemi = str.indexOf(';'); // S5.2 step 1
   var pairRe = options.loose ? LOOSE_COOKIE_PAIR : COOKIE_PAIR;
   var result = pairRe.exec(firstSemi === -1 ? str : str.substr(0,firstSemi));
 
@@ -360,11 +351,11 @@ function parse(str, options) {
   }
 
   // S5.2.3 "unparsed-attributes consist of the remainder of the set-cookie-string
   // (including the %x3B (";") in question)." plus later on in the same section
   // "discard the first ";" and trim".
-  var unparsed = str.slice(firstSemi).replace(/^\s*;\s*/,'').trim();
+  var unparsed = str.slice(firstSemi + 1).trim();
 
   // "If the unparsed-attributes string is empty, skip the rest of these
   // steps."
   if (unparsed.length === 0) {
     return c;
@@ -376,13 +367,16 @@ function parse(str, options) {
    * subsections" for every item.  Plus, for many of the individual attributes
    * in S5.3 it says to use the "attribute-value of the last attribute in the
    * cookie-attribute-list".  Therefore, in this implementation, we overwrite
    * the previous value.
    */
-  var cookie_avs = unparsed.split(/\s*;\s*/);
+  var cookie_avs = unparsed.split(';');
   while (cookie_avs.length) {
-    var av = cookie_avs.shift();
+    var av = cookie_avs.shift().trim();
+    if (av.length === 0) { // happens if ";;" appears
+      continue;
+    }
     var av_sep = av.indexOf('=');
     var av_key, av_value;
 
     if (av_sep === -1) {
       av_key = av;
diff --git a/test/parsing_test.js b/test/parsing_test.js
index c13edb0..966d8b5 100644
--- a/test/parsing_test.js
+++ b/test/parsing_test.js
@@ -320,8 +320,21 @@ vows
       "key": function(c) { assert.equal(c.key, 'foo') },
       "value": function(c) { assert.equal(c.value, 'bar') },
       "no path": function(c) { assert.equal(c.path, null) },
       "no domain": function(c) { assert.equal(c.domain, null) },
       "no extensions": function(c) { assert.ok(!c.extensions) }
+    },
+    "way too many semicolons followed by non-semicolon": {
+      topic: function() {
+        // takes abnormally long due to semi-catastrophic regexp backtracking
+        var str = 'foo=bar' + (';'.repeat(65535)) + ' domain=example.com';
+        return Cookie.parse(str) || null;
+      },
+      "parsed": function(c) { assert.ok(c) },
+      "key": function(c) { assert.equal(c.key, 'foo') },
+      "value": function(c) { assert.equal(c.value, 'bar') },
+      "no path": function(c) { assert.equal(c.path, null) },
+      "no domain": function(c) { assert.equal(c.domain, 'example.com') },
+      "no extensions": function(c) { assert.ok(!c.extensions) }
     }
   })
   .export(module);

Vulnerable code: // Used for checking whether or not there is a trailing semi-colon
var TRAILING_SEMICOLON = /;+$/;

  // S4.1.1 Trailing semi-colons are not part of the specification.
  var semiColonCheck = TRAILING_SEMICOLON.exec(str);
  if (semiColonCheck) {
    str = str.slice(0, semiColonCheck.index);
  }

  var unparsed = str.slice(firstSemi).replace(/^\s*;\s*/,'').trim();
  var cookie_avs = unparsed.split(/\s*;\s*/);
    var av = cookie_avs.shift();
Fixed code:   var unparsed = str.slice(firstSemi + 1).trim();
  var cookie_avs = unparsed.split(';');
    var av = cookie_avs.shift().trim();
    if (av.length === 0) { // happens if ";;" appears
      continue;
    }
    },
    "way too many semicolons followed by non-semicolon": {
      topic: function() {
        // takes abnormally long due to semi-catastrophic regexp backtracking
        var str = 'foo=bar' + (';'.repeat(65535)) + ' domain=example.com';
        return Cookie.parse(str) || null;
      },
      "parsed": function(c) { assert.ok(c) },
      "key": function(c) { assert.equal(c.key, 'foo') },
      "value": function(c) { assert.equal(c.value, 'bar') },
      "no path": function(c) { assert.equal(c.path, null) },
      "no domain": function(c) { assert.equal(c.domain, 'example.com') },
      "no extensions": function(c) { assert.ok(!c.extensions) }