CVE-2022-0512: Authorization Bypass Through User-Controlled Key (CWE-639)
value: Authorization Bypass Through User-Controlled Key in NPM url-parse prior to 1.5.6.
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-14T16:15Z
Repository: https://github.com/unshiftio/url-parse
Language: JavaScript
Files changed: index.js, test/test.js
Commit date: 2022-01-30 09:36:59 +0100
Commit message: [fix] Correctly handle userinfo containing the at sign

Diff: diff --git a/index.js b/index.js
index 702308b..b819461 100644
--- a/index.js
+++ b/index.js
@@ -302,11 +302,15 @@ function Url(address, location, parser) {
     key = instruction[1];
 
     if (parse !== parse) {
       url[key] = address;
     } else if ('string' === typeof parse) {
-      if (~(index = address.indexOf(parse))) {
+      index = parse === '@'
+        ? address.lastIndexOf(parse)
+        : address.indexOf(parse);
+
+      if (~index) {
         if ('number' === typeof instruction[2]) {
           url[key] = address.slice(0, index);
           address = address.slice(index + instruction[2]);
         } else {
           url[key] = address.slice(index);
@@ -368,14 +372,25 @@ function Url(address, location, parser) {
 
   //
   // Parse down the `auth` for the username and password.
   //
   url.username = url.password = '';
+
   if (url.auth) {
-    instruction = url.auth.split(':');
-    url.username = instruction[0];
-    url.password = instruction[1] || '';
+    index = url.auth.indexOf(':');
+
+    if (~index) {
+      url.username = url.auth.slice(0, index);
+      url.username = encodeURIComponent(decodeURIComponent(url.username));
+
+      url.password = url.auth.slice(index + 1);
+      url.password = encodeURIComponent(decodeURIComponent(url.password))
+    } else {
+      url.username = encodeURIComponent(decodeURIComponent(url.auth));
+    }
+
+    url.auth = url.password ? url.username +':'+ url.password : url.username;
   }
 
   url.origin = url.protocol !== 'file:' && isSpecial(url.protocol) && url.host
     ? url.protocol +'//'+ url.host
     : 'null';
@@ -463,13 +478,21 @@ function set(part, value, fn) {
     case 'password':
       url[part] = encodeURIComponent(value);
       break;
 
     case 'auth':
-      var splits = value.split(':');
-      url.username = splits[0];
-      url.password = splits.length === 2 ? splits[1] : '';
+      var index = value.indexOf(':');
+
+      if (~index) {
+        url.username = value.slice(0, index);
+        url.username = encodeURIComponent(decodeURIComponent(url.username));
+
+        url.password = value.slice(index + 1);
+        url.password = encodeURIComponent(decodeURIComponent(url.password));
+      } else {
+        url.username = encodeURIComponent(decodeURIComponent(value));
+      }
   }
 
   for (var i = 0; i < rules.length; i++) {
     var ins = rules[i];
 
diff --git a/test/test.js b/test/test.js
index 8130081..18f16ef 100644
--- a/test/test.js
+++ b/test/test.js
@@ -687,10 +687,58 @@ describe('url-parse', function () {
       assume(parsed.username).equals('user');
       assume(parsed.protocol).equals('http:');
       assume(parsed.hostname).equals('www.example.com');
       assume(parsed.href).equals(url);
     });
+
+    it('handles @ in username', function () {
+      var url = 'http://user@@www.example.com/'
+        , parsed = parse(url);
+
+      assume(parsed.protocol).equals('http:');
+      assume(parsed.auth).equals('user%40');
+      assume(parsed.username).equals('user%40');
+      assume(parsed.password).equals('');
+      assume(parsed.hostname).equals('www.example.com');
+      assume(parsed.pathname).equals('/');
+      assume(parsed.href).equals('http://user%40@www.example.com/');
+
+      url = 'http://user%40@www.example.com/';
+      parsed = parse(url);
+
+      assume(parsed.protocol).equals('http:');
+      assume(parsed.auth).equals('user%40');
+      assume(parsed.username).equals('user%40');
+      assume(parsed.password).equals('');
+      assume(parsed.hostname).equals('www.example.com');
+      assume(parsed.pathname).equals('/');
+      assume(parsed.href).equals('http://user%40@www.example.com/');
+    });
+
+    it('handles @ in password', function () {
+      var url = 'http://user@:pas:s@@www.example.com/'
+        , parsed = parse(url);
+
+      assume(parsed.protocol).equals('http:');
+      assume(parsed.auth).equals('user%40:pas%3As%40');
+      assume(parsed.username).equals('user%40');
+      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/');
+
+      url = 'http://user%40:pas%3As%40@www.example.com/'
+      parsed = parse(url);
+
+      assume(parsed.protocol).equals('http:');
+      assume(parsed.auth).equals('user%40:pas%3As%40');
+      assume(parsed.username).equals('user%40');
+      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('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=');
@@ -1122,10 +1170,30 @@ describe('url-parse', function () {
 
       assume(data.set('auth', ':quux')).equals(data);
       assume(data.username).equals('');
       assume(data.password).equals('quux');
       assume(data.href).equals('https://:quux@example.com/');
+
+      assume(data.set('auth', 'user@:pass@')).equals(data);
+      assume(data.username).equals('user%40');
+      assume(data.password).equals('pass%40');
+      assume(data.href).equals('https://user%40:pass%40@example.com/');
+
+      assume(data.set('auth', 'user%40:pass%40')).equals(data);
+      assume(data.username).equals('user%40');
+      assume(data.password).equals('pass%40');
+      assume(data.href).equals('https://user%40:pass%40@example.com/');
+
+      assume(data.set('auth', 'user:pass:word')).equals(data);
+      assume(data.username).equals('user');
+      assume(data.password).equals('pass%3Aword');
+      assume(data.href).equals('https://user:pass%3Aword@example.com/');
+
+      assume(data.set('auth', 'user:pass%3Aword')).equals(data);
+      assume(data.username).equals('user');
+      assume(data.password).equals('pass%3Aword');
+      assume(data.href).equals('https://user:pass%3Aword@example.com/');
     });
 
     it('updates other values', function () {
       var data = parse('http://google.com/?foo=bar');
 

Vulnerable code:       if (~(index = address.indexOf(parse))) {
    instruction = url.auth.split(':');
    url.username = instruction[0];
    url.password = instruction[1] || '';
      var splits = value.split(':');
      url.username = splits[0];
      url.password = splits.length === 2 ? splits[1] : '';
Fixed code:       index = parse === '@'
        ? address.lastIndexOf(parse)
        : address.indexOf(parse);

      if (~index) {

    index = url.auth.indexOf(':');

    if (~index) {
      url.username = url.auth.slice(0, index);
      url.username = encodeURIComponent(decodeURIComponent(url.username));

      url.password = url.auth.slice(index + 1);
      url.password = encodeURIComponent(decodeURIComponent(url.password))
    } else {
      url.username = encodeURIComponent(decodeURIComponent(url.auth));
    }

    url.auth = url.password ? url.username +':'+ url.password : url.username;
      var index = value.indexOf(':');

      if (~index) {
        url.username = value.slice(0, index);
        url.username = encodeURIComponent(decodeURIComponent(url.username));

        url.password = value.slice(index + 1);
        url.password = encodeURIComponent(decodeURIComponent(url.password));
      } else {
        url.username = encodeURIComponent(decodeURIComponent(value));
      }

    it('handles @ in username', function () {
      var url = 'http://user@@www.example.com/'
        , parsed = parse(url);

      assume(parsed.protocol).equals('http:');
      assume(parsed.auth).equals('user%40');
      assume(parsed.username).equals('user%40');
      assume(parsed.password).equals('');
      assume(parsed.hostname).equals('www.example.com');
      assume(parsed.pathname).equals('/');
      assume(parsed.href).equals('http://user%40@www.example.com/');

      url = 'http://user%40@www.example.com/';
      parsed = parse(url);

      assume(parsed.protocol).equals('http:');
      assume(parsed.auth).equals('user%40');
      assume(parsed.username).equals('user%40');
      assume(parsed.password).equals('');
      assume(parsed.hostname).equals('www.example.com');
      assume(parsed.pathname).equals('/');
      assume(parsed.href).equals('http://user%40@www.example.com/');
    });

    it('handles @ in password', function () {
      var url = 'http://user@:pas:s@@www.example.com/'
        , parsed = parse(url);

      assume(parsed.protocol).equals('http:');
      assume(parsed.auth).equals('user%40:pas%3As%40');
      assume(parsed.username).equals('user%40');
      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/');

      url = 'http://user%40:pas%3As%40@www.example.com/'
      parsed = parse(url);

      assume(parsed.protocol).equals('http:');
      assume(parsed.auth).equals('user%40:pas%3As%40');
      assume(parsed.username).equals('user%40');
      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/');
    });

      assume(data.set('auth', 'user@:pass@')).equals(data);
      assume(data.username).equals('user%40');
      assume(data.password).equals('pass%40');
      assume(data.href).equals('https://user%40:pass%40@example.com/');

      assume(data.set('auth', 'user%40:pass%40')).equals(data);
      assume(data.username).equals('user%40');
      assume(data.password).equals('pass%40');
      assume(data.href).equals('https://user%40:pass%40@example.com/');

      assume(data.set('auth', 'user:pass:word')).equals(data);
      assume(data.username).equals('user');
      assume(data.password).equals('pass%3Aword');
      assume(data.href).equals('https://user:pass%3Aword@example.com/');

      assume(data.set('auth', 'user:pass%3Aword')).equals(data);
      assume(data.username).equals('user');
      assume(data.password).equals('pass%3Aword');
      assume(data.href).equals('https://user:pass%3Aword@example.com/');