CVE-2018-7651: Uncontrolled Resource Consumption (CWE-400)
value: index.js in the ssri module before 5.2.2 for Node.js is prone to a regular expression denial of service vulnerability in strict mode functionality via a long base64 hash string.
CWE description: The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
CVSS v3 base score: 5.9
Published: 2018-03-04T01:29Z
Repository: https://github.com/zkat/ssri
Language: JavaScript
Files changed: index.js, test/integrity.js, test/stringify.js
Commit date: 2018-02-14 12:35:23 -0800
Commit message: fix(security): tweak strict SRI regex (#10)

The previous form was vulnerable to ReDoS attacks, by
crafting exceptionally long base64 hash strings.

This issue only affected consumers using the opts.strict option.
Diff: diff --git a/index.js b/index.js
index ba1bb6d..43c1507 100644
--- a/index.js
+++ b/index.js
@@ -7,11 +7,11 @@ const Transform = require('stream').Transform
 
 const SPEC_ALGORITHMS = ['sha256', 'sha384', 'sha512']
 
 const BASE64_REGEX = /^[a-z0-9+/]+(?:=?=?)$/i
 const SRI_REGEX = /^([^-]+)-([^?]+)([?\S*]*)$/
-const STRICT_SRI_REGEX = /^([^-]+)-([A-Za-z0-9+/]+(?:=?=?))([?\x21-\x7E]*)$/
+const STRICT_SRI_REGEX = /^([^-]+)-([A-Za-z0-9+/=]{44,88})(\?[\x21-\x7E]*)*$/
 const VCHAR_REGEX = /^[\x21-\x7E]+$/
 
 class Hash {
   get isHash () { return true }
   constructor (hash, opts) {
diff --git a/test/integrity.js b/test/integrity.js
index 3a697fb..a35f725 100644
--- a/test/integrity.js
+++ b/test/integrity.js
@@ -5,24 +5,24 @@ const Buffer = require('safe-buffer').Buffer
 const test = require('tap').test
 
 const ssri = require('..')
 
 test('toString()', t => {
-  const sri = ssri.parse('sha512-foo sha256-bar!')
+  const sri = ssri.parse('sha1-eUN/Xt2hP5wGabl43XqQZt0gWfE= sha256-Qhx213Vjr6GRSEawEL0WTzlb00whAuXpngy5zxc8HYc=')
   t.equal(
     sri.toString(),
-    'sha512-foo sha256-bar!',
+    'sha1-eUN/Xt2hP5wGabl43XqQZt0gWfE= sha256-Qhx213Vjr6GRSEawEL0WTzlb00whAuXpngy5zxc8HYc=',
     'integrity objects from ssri.parse() can use toString()'
   )
   t.equal(
     sri.toString({strict: true}),
-    'sha512-foo',
+    'sha256-Qhx213Vjr6GRSEawEL0WTzlb00whAuXpngy5zxc8HYc=',
     'accepts strict mode option'
   )
   t.equal(
     sri.toString({sep: '\n'}),
-    'sha512-foo\nsha256-bar!',
+    'sha1-eUN/Xt2hP5wGabl43XqQZt0gWfE=\nsha256-Qhx213Vjr6GRSEawEL0WTzlb00whAuXpngy5zxc8HYc=',
     'accepts separator option'
   )
   t.done()
 })
 
@@ -70,13 +70,16 @@ test('concat()', t => {
       'sha512-quux'
     ).toString(),
     'sha512-foo sha512-quux sha1-bar sha1-baz',
     'preserves relative order for algorithms between different concatenations'
   )
+  const strictSri = ssri.parse('sha512-WrLorGiX4iEWOOOaJSiCrmDIamA47exH+Bz7tVwIPb4sCU8w4iNqGCqYuspMMeU5pgz/sU7koP5u8W3RCUojGw==')
   t.equal(
-    sri.concat('sha1-bar!', {strict: true}).toString(),
-    'sha512-foo',
+    strictSri.concat('sha1-eUN/Xt2hP5wGabl43XqQZt0gWfE=', {
+      strict: true
+    }).toString(),
+    'sha512-WrLorGiX4iEWOOOaJSiCrmDIamA47exH+Bz7tVwIPb4sCU8w4iNqGCqYuspMMeU5pgz/sU7koP5u8W3RCUojGw==',
     'accepts strict mode option'
   )
   t.done()
 })
 
diff --git a/test/stringify.js b/test/stringify.js
index 727efbd..53200cb 100644
--- a/test/stringify.js
+++ b/test/stringify.js
@@ -106,11 +106,11 @@ test('support strict serialization', t => {
     ssri.stringify(sriLike, {strict: true}),
     `sha256-${hash(TEST_DATA, 'sha256')}?foo`,
     'entries that do not conform to strict spec interpretation removed'
   )
   t.equal(
-    ssri.stringify('sha512-foo sha256-bar', {sep: ' \r|\n\t', strict: true}),
-    'sha512-foo \r \n\tsha256-bar',
+    ssri.stringify('sha512-WrLorGiX4iEWOOOaJSiCrmDIamA47exH+Bz7tVwIPb4sCU8w4iNqGCqYuspMMeU5pgz/sU7koP5u8W3RCUojGw== sha256-Qhx213Vjr6GRSEawEL0WTzlb00whAuXpngy5zxc8HYc=', {sep: ' \r|\n\t', strict: true}),
+    'sha512-WrLorGiX4iEWOOOaJSiCrmDIamA47exH+Bz7tVwIPb4sCU8w4iNqGCqYuspMMeU5pgz/sU7koP5u8W3RCUojGw== \r \n\tsha256-Qhx213Vjr6GRSEawEL0WTzlb00whAuXpngy5zxc8HYc=',
     'strict mode replaces non-whitespace characters in separator with space'
   )
   t.done()
 })

Vulnerable code: const STRICT_SRI_REGEX = /^([^-]+)-([A-Za-z0-9+/]+(?:=?=?))([?\x21-\x7E]*)$/
  const sri = ssri.parse('sha512-foo sha256-bar!')
    'sha512-foo sha256-bar!',
    'sha512-foo',
    'sha512-foo\nsha256-bar!',
    sri.concat('sha1-bar!', {strict: true}).toString(),
    'sha512-foo',
    ssri.stringify('sha512-foo sha256-bar', {sep: ' \r|\n\t', strict: true}),
    'sha512-foo \r \n\tsha256-bar',
Fixed code: const STRICT_SRI_REGEX = /^([^-]+)-([A-Za-z0-9+/=]{44,88})(\?[\x21-\x7E]*)*$/
  const sri = ssri.parse('sha1-eUN/Xt2hP5wGabl43XqQZt0gWfE= sha256-Qhx213Vjr6GRSEawEL0WTzlb00whAuXpngy5zxc8HYc=')
    'sha1-eUN/Xt2hP5wGabl43XqQZt0gWfE= sha256-Qhx213Vjr6GRSEawEL0WTzlb00whAuXpngy5zxc8HYc=',
    'sha256-Qhx213Vjr6GRSEawEL0WTzlb00whAuXpngy5zxc8HYc=',
    'sha1-eUN/Xt2hP5wGabl43XqQZt0gWfE=\nsha256-Qhx213Vjr6GRSEawEL0WTzlb00whAuXpngy5zxc8HYc=',
  const strictSri = ssri.parse('sha512-WrLorGiX4iEWOOOaJSiCrmDIamA47exH+Bz7tVwIPb4sCU8w4iNqGCqYuspMMeU5pgz/sU7koP5u8W3RCUojGw==')
    strictSri.concat('sha1-eUN/Xt2hP5wGabl43XqQZt0gWfE=', {
      strict: true
    }).toString(),
    'sha512-WrLorGiX4iEWOOOaJSiCrmDIamA47exH+Bz7tVwIPb4sCU8w4iNqGCqYuspMMeU5pgz/sU7koP5u8W3RCUojGw==',
    ssri.stringify('sha512-WrLorGiX4iEWOOOaJSiCrmDIamA47exH+Bz7tVwIPb4sCU8w4iNqGCqYuspMMeU5pgz/sU7koP5u8W3RCUojGw== sha256-Qhx213Vjr6GRSEawEL0WTzlb00whAuXpngy5zxc8HYc=', {sep: ' \r|\n\t', strict: true}),
    'sha512-WrLorGiX4iEWOOOaJSiCrmDIamA47exH+Bz7tVwIPb4sCU8w4iNqGCqYuspMMeU5pgz/sU7koP5u8W3RCUojGw== \r \n\tsha256-Qhx213Vjr6GRSEawEL0WTzlb00whAuXpngy5zxc8HYc=',