CVE-2022-0639: Authorization Bypass Through User-Controlled Key (CWE-639)
value: Authorization Bypass Through User-Controlled Key in NPM url-parse prior to 1.5.7.
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: 5.3
Published: 2022-02-17T18:15Z
Repository: https://github.com/unshiftio/url-parse
Language: JavaScript
Files changed: index.js, test/test.js
Commit date: 2022-02-16 19:05:36 +0100
Commit message: [fix] Readd the empty userinfo to `url.href` (#226)

If the userinfo is present but empty, the parsed host is also empty, and
`url.pathname` is not `'/'`, then readd the empty userinfo to `url.href`,
otherwise the original invalid URL might be transformed into a valid one
with `url.pathname` as host.
Diff: diff --git a/index.js b/index.js
index 517b6b6..d808b13 100644
--- a/index.js
+++ b/index.js
@@ -537,10 +537,21 @@ function toString(stringify) {
     if (url.password) result += ':'+ url.password;
     result += '@';
   } else if (url.password) {
     result += ':'+ url.password;
     result += '@';
+  } else if (
+    url.protocol !== 'file:' &&
+    isSpecial(url.protocol) &&
+    !url.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;
 
   query = 'object' === typeof url.query ? stringify(url.query) : url.query;
diff --git a/test/test.js b/test/test.js
index 6d53eb9..98880b3 100644
--- a/test/test.js
+++ b/test/test.js
@@ -769,10 +769,69 @@ describe('url-parse', function () {
       assume(parsed.password).equals('pas%3As%40');
       assume(parsed.hostname).equals('www.example.com');
       assume(parsed.pathname).equals('/');
       assume(parsed.href).equals('http://user%40:pas%3As%40@www.example.com/');
     });
+
+    it('adds @ to href if auth and host are empty', function () {
+      var parsed, i = 0;
+      var urls = [
+        'http:@/127.0.0.1',
+        'http::@/127.0.0.1',
+        'http:/@/127.0.0.1',
+        'http:/:@/127.0.0.1',
+        'http://@/127.0.0.1',
+        'http://:@/127.0.0.1',
+        'http:///@/127.0.0.1',
+        'http:///:@/127.0.0.1'
+      ];
+
+      for (; i < urls.length; i++) {
+        parsed = parse(urls[i]);
+
+        assume(parsed.protocol).equals('http:');
+        assume(parsed.auth).equals('');
+        assume(parsed.username).equals('');
+        assume(parsed.password).equals('');
+        assume(parsed.host).equals('');
+        assume(parsed.hostname).equals('');
+        assume(parsed.pathname).equals('/127.0.0.1');
+        assume(parsed.origin).equals('null');
+        assume(parsed.href).equals('http://@/127.0.0.1');
+        assume(parsed.toString()).equals('http://@/127.0.0.1');
+      }
+
+      urls = [
+        'http:@/',
+        'http:@',
+        'http::@/',
+        'http::@',
+        'http:/@/',
+        'http:/@',
+        'http:/:@/',
+        'http:/:@',
+        'http://@/',
+        'http://@',
+        'http://:@/',
+        'http://:@'
+      ];
+
+      for (i = 0; i < urls.length; i++) {
+        parsed = parse(urls[i]);
+
+        assume(parsed.protocol).equals('http:');
+        assume(parsed.auth).equals('');
+        assume(parsed.username).equals('');
+        assume(parsed.password).equals('');
+        assume(parsed.host).equals('');
+        assume(parsed.hostname).equals('');
+        assume(parsed.pathname).equals('/');
+        assume(parsed.origin).equals('null');
+        assume(parsed.href).equals('http:///');
+        assume(parsed.toString()).equals('http:///');
+      }
+    });
   });
 
   it('accepts multiple ???', function () {
     var url = 'http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=';
     assume(parse(url).query).equals('???&hl=en&src=api&x=2&y=2&z=3&s=');

Fixed code:   } else if (
    url.protocol !== 'file:' &&
    isSpecial(url.protocol) &&
    !url.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 += '@';

    it('adds @ to href if auth and host are empty', function () {
      var parsed, i = 0;
      var urls = [
        'http:@/127.0.0.1',
        'http::@/127.0.0.1',
        'http:/@/127.0.0.1',
        'http:/:@/127.0.0.1',
        'http://@/127.0.0.1',
        'http://:@/127.0.0.1',
        'http:///@/127.0.0.1',
        'http:///:@/127.0.0.1'
      ];

      for (; i < urls.length; i++) {
        parsed = parse(urls[i]);

        assume(parsed.protocol).equals('http:');
        assume(parsed.auth).equals('');
        assume(parsed.username).equals('');
        assume(parsed.password).equals('');
        assume(parsed.host).equals('');
        assume(parsed.hostname).equals('');
        assume(parsed.pathname).equals('/127.0.0.1');
        assume(parsed.origin).equals('null');
        assume(parsed.href).equals('http://@/127.0.0.1');
        assume(parsed.toString()).equals('http://@/127.0.0.1');
      }

      urls = [
        'http:@/',
        'http:@',
        'http::@/',
        'http::@',
        'http:/@/',
        'http:/@',
        'http:/:@/',
        'http:/:@',
        'http://@/',
        'http://@',
        'http://:@/',
        'http://:@'
      ];

      for (i = 0; i < urls.length; i++) {
        parsed = parse(urls[i]);

        assume(parsed.protocol).equals('http:');
        assume(parsed.auth).equals('');
        assume(parsed.username).equals('');
        assume(parsed.password).equals('');
        assume(parsed.host).equals('');
        assume(parsed.hostname).equals('');
        assume(parsed.pathname).equals('/');
        assume(parsed.origin).equals('null');
        assume(parsed.href).equals('http:///');
        assume(parsed.toString()).equals('http:///');
      }
    });