CVE-2018-25079: Inefficient Regular Expression Complexity (CWE-1333)
value: A vulnerability was found in Segmentio is-url up to 1.2.2. It has been rated as problematic. Affected by this issue is some unknown functionality of the file index.js. The manipulation leads to inefficient regular expression complexity. The attack may be launched remotely. Upgrading to version 1.2.3 is able to address this issue. The patch is identified as 149550935c63a98c11f27f694a7c4a9479e53794. It is recommended to upgrade the affected component. VDB-220058 is the identifier assigned to this vulnerability.
CWE description: The product uses a regular expression with an inefficient, possibly exponential worst-case computational complexity that consumes excessive CPU cycles.
CVSS v3 base score: 7.5
Published: 2023-02-04T04:15Z
Repository: https://github.com/segmentio/is-url
Language: JavaScript
Files changed: index.js, test/index.js
Commit date: 2018-03-19 17:08:52 -0400
Commit message: security: Fix REDOS vulnerability

Problem:
As disclosed by email, the regex in this module was vulnerable to
catastrophic backtracking.
This could be used as a REDOS vector for Node.js servers that use
this module.

Solution:
Use a two-step validation process.
Split the vulnerable regex into three safe ones.

Diff: diff --git a/index.js b/index.js
index 1ef5187..1d3e349 100644
--- a/index.js
+++ b/index.js
@@ -4,20 +4,40 @@
  */
 
 module.exports = isUrl;
 
 /**
- * Matcher.
+ * RegExps.
+ * A URL must match #1 and then at least one of #2/#3.
+ * Use two levels of REs to avoid REDOS.
  */
 
-var matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/;
+var protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/;
+
+var localhostDomainRE = /^localhost[\:?\d]*(?:[^\:?\d]\S*)?$/
+var nonLocalhostDomainRE = /^[^\s\.]+\.\S{2,}$/;
 
 /**
  * Loosely validate a URL `string`.
  *
  * @param {String} string
  * @return {Boolean}
  */
 
 function isUrl(string){
-  return matcher.test(string);
+  var match = string.match(protocolAndDomainRE);
+  if (!match) {
+    return false;
+  }
+
+  var everythingAfterProtocol = match[1];
+  if (!everythingAfterProtocol) {
+    return false;
+  }
+
+  if (localhostDomainRE.test(everythingAfterProtocol) ||
+      nonLocalhostDomainRE.test(everythingAfterProtocol)) {
+    return true;
+  }
+
+  return false;
 }
diff --git a/test/index.js b/test/index.js
index 5f7aebc..52cfacb 100644
--- a/test/index.js
+++ b/test/index.js
@@ -117,6 +117,17 @@ describe('is-url', function () {
 
     it('google.com', function () {
       assert(!url('google.com'));
     });
   });
+
+  describe('redos', function () {
+    it('redos exploit', function () {
+      // Invalid. This should be discovered in under 1 second.
+      var attackString = 'a://localhost' + '9'.repeat(100000) + '\t';
+      var before = process.hrtime();
+      assert(!url(attackString), 'attackString was valid');
+      var elapsed = process.hrtime(before);
+      assert(elapsed[0] < 1, 'attackString took ' + elapsed[0] + ' > 1 seconds');
+    });
+  });
 });

Vulnerable code:  * Matcher.
var matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/;
  return matcher.test(string);
Fixed code:  * RegExps.
 * A URL must match #1 and then at least one of #2/#3.
 * Use two levels of REs to avoid REDOS.
var protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/;

var localhostDomainRE = /^localhost[\:?\d]*(?:[^\:?\d]\S*)?$/
var nonLocalhostDomainRE = /^[^\s\.]+\.\S{2,}$/;
  var match = string.match(protocolAndDomainRE);
  if (!match) {
    return false;
  }

  var everythingAfterProtocol = match[1];
  if (!everythingAfterProtocol) {
    return false;
  }

  if (localhostDomainRE.test(everythingAfterProtocol) ||
      nonLocalhostDomainRE.test(everythingAfterProtocol)) {
    return true;
  }

  return false;

  describe('redos', function () {
    it('redos exploit', function () {
      // Invalid. This should be discovered in under 1 second.
      var attackString = 'a://localhost' + '9'.repeat(100000) + '\t';
      var before = process.hrtime();
      assert(!url(attackString), 'attackString was valid');
      var elapsed = process.hrtime(before);
      assert(elapsed[0] < 1, 'attackString took ' + elapsed[0] + ' > 1 seconds');
    });
  });