CVE-2022-0686: Authorization Bypass Through User-Controlled Key (CWE-639)
value: Authorization Bypass Through User-Controlled Key in NPM url-parse prior to 1.5.8.
CWE description: The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.
CVSS v3 base score: 9.1
Published: 2022-02-20T13:15Z
Repository: https://github.com/unshiftio/url-parse
Language: JavaScript
Files changed: index.js, test/test.js
Commit date: 2022-02-19 09:51:04 +0100
Commit message: [fix] Handle the case where the port is specified but empty

Diff: diff --git a/index.js b/index.js
index d808b13..c5a2a11 100644
--- a/index.js
+++ b/index.js
@@ -37,11 +37,11 @@ var rules = [
     return isSpecial(url.protocol) ? address.replace(/\\/g, '/') : address;
   },
   ['/', 'pathname'],                    // Extract from the back.
   ['@', 'auth', 1],                     // Extract from the front.
   [NaN, 'host', undefined, 1, 1],       // Set left over value.
-  [/:(\d+)$/, 'port', undefined, 1],    // RegExp the back.
+  [/:(\d*)$/, 'port', undefined, 1],    // RegExp the back.
   [NaN, 'hostname', undefined, 1, 1]    // Set left over.
 ];
 
 /**
  * These properties should not be copied or inherited from. This is only needed
@@ -522,10 +522,11 @@ function set(part, value, fn) {
 function toString(stringify) {
   if (!stringify || 'function' !== typeof stringify) stringify = qs.stringify;
 
   var query
     , url = this
+    , host = url.host
     , protocol = url.protocol;
 
   if (protocol && protocol.charAt(protocol.length - 1) !== ':') protocol += ':';
 
   var result =
@@ -540,21 +541,27 @@ function toString(stringify) {
     result += ':'+ url.password;
     result += '@';
   } else if (
     url.protocol !== 'file:' &&
     isSpecial(url.protocol) &&
-    !url.host &&
+    !host &&
     url.pathname !== '/'
   ) {
     //
     // Add back the empty userinfo, otherwise the original invalid URL
     // might be transformed into a valid one with `url.pathname` as host.
     //
     result += '@';
   }
 
-  result += url.host + url.pathname;
+  //
+  // Trailing colon is removed from `url.host` when it is parsed. If it still
+  // ends with a colon, then add back the trailing colon that was removed. This
+  // prevents an invalid URL from being transformed into a valid one.
+  //
+  if (host[host.length - 1] === ':') host += ':';
+  result += host + url.pathname;
 
   query = 'object' === typeof url.query ? stringify(url.query) : url.query;
   if (query) result += '?' !== query.charAt(0) ? '?'+ query : query;
 
   if (url.hash) result += url.hash;
diff --git a/test/test.js b/test/test.js
index 98880b3..ab2c6c3 100644
--- a/test/test.js
+++ b/test/test.js
@@ -440,10 +440,32 @@ describe('url-parse', function () {
     assume(parsed.protocol).equals('sip:');
     assume(parsed.pathname).equals('alice@atlanta.com');
     assume(parsed.href).equals('sip:alice@atlanta.com');
   });
 
+  it('handles the case where the port is specified but empty', function () {
+    var parsed = parse('http://example.com:');
+
+    assume(parsed.protocol).equals('http:');
+    assume(parsed.port).equals('');
+    assume(parsed.host).equals('example.com');
+    assume(parsed.hostname).equals('example.com');
+    assume(parsed.pathname).equals('/');
+    assume(parsed.origin).equals('http://example.com');
+    assume(parsed.href).equals('http://example.com/');
+
+    parsed = parse('http://example.com::');
+
+    assume(parsed.protocol).equals('http:');
+    assume(parsed.port).equals('');
+    assume(parsed.host).equals('example.com:');
+    assume(parsed.hostname).equals('example.com:');
+    assume(parsed.pathname).equals('/');
+    assume(parsed.origin).equals('http://example.com:');
+    assume(parsed.href).equals('http://example.com::/');
+  });
+
   describe('origin', function () {
     it('generates an origin property', function () {
       var url = 'http://google.com:80/pathname'
         , parsed = parse(url);
 

Vulnerable code:   [/:(\d+)$/, 'port', undefined, 1],    // RegExp the back.
    !url.host &&
  result += url.host + url.pathname;
Fixed code:   [/:(\d*)$/, 'port', undefined, 1],    // RegExp the back.
    , host = url.host
    !host &&
  //
  // Trailing colon is removed from `url.host` when it is parsed. If it still
  // ends with a colon, then add back the trailing colon that was removed. This
  // prevents an invalid URL from being transformed into a valid one.
  //
  if (host[host.length - 1] === ':') host += ':';
  result += host + url.pathname;
  it('handles the case where the port is specified but empty', function () {
    var parsed = parse('http://example.com:');

    assume(parsed.protocol).equals('http:');
    assume(parsed.port).equals('');
    assume(parsed.host).equals('example.com');
    assume(parsed.hostname).equals('example.com');
    assume(parsed.pathname).equals('/');
    assume(parsed.origin).equals('http://example.com');
    assume(parsed.href).equals('http://example.com/');

    parsed = parse('http://example.com::');

    assume(parsed.protocol).equals('http:');
    assume(parsed.port).equals('');
    assume(parsed.host).equals('example.com:');
    assume(parsed.hostname).equals('example.com:');
    assume(parsed.pathname).equals('/');
    assume(parsed.origin).equals('http://example.com:');
    assume(parsed.href).equals('http://example.com::/');
  });