CVE-2021-27515: Insufficient Information (NVD-CWE-noinfo)
value: url-parse before 1.5.0 mishandles certain uses of backslash such as http:\/ and interprets the URI as a relative path.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 5.3
Published: 2021-02-22T00:15Z
Repository: https://github.com/unshiftio/url-parse
Language: JavaScript
Files changed: SECURITY.md, index.js, test/fuzzy.js, test/test.js
Commit date: 2021-02-17 16:17:44 +0100
Commit message: [security] More backslash fixes (#197)


Diff: diff --git a/SECURITY.md b/SECURITY.md
index a1c3d63..31ef5b4 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -31,17 +31,26 @@ Once we have acknowledged receipt of your report and confirmed the bug
 ourselves we will work with you to fix the vulnerability and publicly
 acknowledge your responsible disclosure, if you wish.
 
 ## History
 
+> Using backslash in the protocol is valid in the browser, while url-parse
+> thinks it’s a relative path. An application that validates a url using
+> url-parse might pass a malicious link.
+
+- **Reporter credits**
+  - CxSCA AppSec team at Checkmarx.
+  - Twitter: [Yaniv Nizry](https://twitter.com/ynizry)
+- Fixed in: 1.5.0
+
 > The `extractProtocol` method does not return the correct protocol when
 > provided with unsanitized content which could lead to false positives.
 
 - **Reporter credits**
   - Reported through our security email & Twitter interaction.
   - Twitter: [@ronperris](https://twitter.com/ronperris)
-  - Fixed in: 1.4.5
+- Fixed in: 1.4.5
 
 ---
 
 > url-parse returns wrong hostname which leads to multiple vulnerabilities such
 > as SSRF, Open Redirect, Bypass Authentication Protocol.
diff --git a/index.js b/index.js
index 9e58eda..e54575a 100644
--- a/index.js
+++ b/index.js
@@ -1,11 +1,11 @@
 'use strict';
 
 var required = require('requires-port')
   , qs = require('querystringify')
-  , slashes = /^[A-Za-z][A-Za-z0-9+-.]*:\/\//
-  , protocolre = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\S\s]*)/i
+  , slashes = /^[A-Za-z][A-Za-z0-9+-.]*:[\\/]+/
+  , protocolre = /^([a-z][a-z0-9.+-]*:)?([\\/]{1,})?([\S\s]*)/i
   , whitespace = '[\\x09\\x0A\\x0B\\x0C\\x0D\\x20\\xA0\\u1680\\u180E\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200A\\u202F\\u205F\\u3000\\u2028\\u2029\\uFEFF]'
   , left = new RegExp('^'+ whitespace +'+');
 
 /**
  * Trim a given string.
@@ -113,15 +113,18 @@ function lolcation(loc) {
  * @return {ProtocolExtract} Extracted information.
  * @private
  */
 function extractProtocol(address) {
   address = trimLeft(address);
-  var match = protocolre.exec(address);
+
+  var match = protocolre.exec(address)
+    , protocol = match[1] ? match[1].toLowerCase() : ''
+    , slashes = !!(match[2] && match[2].length >= 2);
 
   return {
-    protocol: match[1] ? match[1].toLowerCase() : '',
-    slashes: !!match[2],
+    protocol: protocol,
+    slashes: slashes,
     rest: match[3]
   };
 }
 
 /**
@@ -278,10 +281,18 @@ function Url(address, location, parser) {
     && (url.pathname !== '' || location.pathname !== '')
   ) {
     url.pathname = resolve(url.pathname, location.pathname);
   }
 
+  //
+  // Default to a / for pathname if none exists. This normalizes the URL
+  // to always have a /
+  //
+  if (url.pathname.charAt(0) !== '/' && url.hostname) {
+    url.pathname = '/' + url.pathname;
+  }
+
   //
   // We should not add port numbers if they are already the default port number
   // for a given protocol. As the host also contains the port number we're going
   // override it with the hostname which contains no port number.
   //
diff --git a/test/fuzzy.js b/test/fuzzy.js
index f0990d3..6052040 100644
--- a/test/fuzzy.js
+++ b/test/fuzzy.js
@@ -101,10 +101,12 @@ function yep() {
 module.exports = function generate() {
   var spec = {}
     , key;
 
   spec.protocol = get('protocol');
+  spec.slashes = true;
+  
   spec.hostname = get('hostname');
   spec.pathname = get('pathname');
 
   if (yep()) spec.port = get('port');
   if (yep()) spec.query = '?'+ get('query');
diff --git a/test/test.js b/test/test.js
index 977fa3c..2161761 100644
--- a/test/test.js
+++ b/test/test.js
@@ -188,13 +188,14 @@ describe('url-parse', function () {
   it('removes default port numbers', function () {
     var url = 'http://example.com:80'
       , parsed = parse(url);
 
     assume(parsed.port).equals('');
+    assume(parsed.pathname).equals('/');
     assume(parsed.host).equals('example.com');
     assume(parsed.hostname).equals('example.com');
-    assume(parsed.href).equals('http://example.com');
+    assume(parsed.href).equals('http://example.com/');
   });
 
   it('understands an / as pathname', function () {
     var url = 'http://example.com:80/'
       , parsed = parse(url);
@@ -240,20 +241,34 @@ describe('url-parse', function () {
     assume(parsed.username).equals('');
     assume(parsed.password).equals('');
     assume(parsed.hostname).equals('google.com');
     assume(parsed.hash).equals('#what\\is going on');
 
-    parsed = parse('//\\what-is-up.com');
+    parsed = parse('http://yolo.com\\what-is-up.com');
     assume(parsed.pathname).equals('/what-is-up.com');
   });
 
   it('correctly ignores multiple slashes //', function () {
     var url = '////what-is-up.com'
       , parsed = parse(url);
 
-    assume(parsed.host).equals('');
-    assume(parsed.hostname).equals('');
+    assume(parsed.host).equals('what-is-up.com');
+    assume(parsed.href).equals('//what-is-up.com/');
+  });
+
+  it('does not see a slash after the protocol as path', function () {
+    var url = 'https:\\/github.com/foo/bar'
+      , parsed = parse(url);
+
+    assume(parsed.host).equals('github.com');
+    assume(parsed.hostname).equals('github.com');
+    assume(parsed.pathname).equals('/foo/bar');
+
+    url = 'https:/\/\/\github.com/foo/bar';
+    assume(parsed.host).equals('github.com');
+    assume(parsed.hostname).equals('github.com');
+    assume(parsed.pathname).equals('/foo/bar');
   });
 
   describe('origin', function () {
     it('generates an origin property', function () {
       var url = 'http://google.com:80/pathname'
@@ -325,36 +340,56 @@ describe('url-parse', function () {
 
   describe('protocol', function () {
     it('extracts the right protocol from a url', function () {
       var testData = [
         {
-          href: 'http://example.com',
+          href: 'http://example.com/',
           protocol: 'http:',
-          pathname: ''
+          pathname: '/',
+          slashes: true
+        },
+        {
+          href: 'ws://example.com/',
+          protocol: 'ws:',
+          pathname: '/',
+          slashes: true
+        },
+        {
+          href: 'wss://example.com/',
+          protocol: 'wss:',
+          pathname: '/',
+          slashes: true
         },
         {
           href: 'mailto:test@example.com',
           pathname: 'test@example.com',
-          protocol: 'mailto:'
+          protocol: 'mailto:',
+          slashes: false
         },
         {
           href: 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E',
           pathname: 'text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E',
-          protocol: 'data:'
+          protocol: 'data:',
+          slashes: false,
         },
         {
           href: 'sip:alice@atlanta.com',
           pathname: 'alice@atlanta.com',
-          protocol: 'sip:'
+          protocol: 'sip:',
+          slashes: false,
         }
       ];
 
-      var data;
+      var data, test;
       for (var i = 0, len = testData.length; i < len; ++i) {
-        data = parse(testData[i].href);
-        assume(data.protocol).equals(testData[i].protocol);
-        assume(data.pathname).equals(testData[i].pathname);
+        test = testData[i];
+        data = parse(test.href);
+
+        assume(data.protocol).equals(test.protocol);
+        assume(data.pathname).equals(test.pathname);
+        assume(data.slashes).equals(test.slashes);
+        assume(data.href).equals(test.href);
       }
     });
 
     it('converts protocol to lowercase', function () {
       var url = 'HTTP://example.com';
@@ -389,17 +424,18 @@ describe('url-parse', function () {
       assume(parsed.pathname).equals('/foo/bar');
       assume(parsed.href).equals('http://[1080:0:0:0:8:800:200c:417a]:61616/foo/bar?q=z');
     });
 
     it('parses ipv6 with auth', function () {
-      var url = 'http://user:password@[3ffe:2a00:100:7031::1]:8080'
+      var url = 'http://user:password@[3ffe:2a00:100:7031::1]:8080/'
         , parsed = parse(url);
 
       assume(parsed.username).equals('user');
       assume(parsed.password).equals('password');
       assume(parsed.host).equals('[3ffe:2a00:100:7031::1]:8080');
       assume(parsed.hostname).equals('[3ffe:2a00:100:7031::1]');
+      assume(parsed.pathname).equals('/');
       assume(parsed.href).equals(url);
     });
 
     it('parses ipv4', function () {
       var url = 'http://222.148.142.13:61616/foo/bar?q=z'
@@ -465,11 +501,11 @@ describe('url-parse', function () {
     it('does not inherit port numbers for non relative urls', function () {
       var data = parse('http://localhost', parse('http://sub.example.com:808/'));
 
       assume(data.port).equals('');
       assume(data.host).equals('localhost');
-      assume(data.href).equals('http://localhost');
+      assume(data.href).equals('http://localhost/');
     });
 
     it('inherits port numbers for relative urls', function () {
       var data = parse('/foo', parse('http://sub.example.com:808/'));
 
@@ -514,11 +550,12 @@ describe('url-parse', function () {
       assume(data.slashes).equals(true);
       assume(data.href).equals('http://example.com/foo');
     });
 
     it('inherits protocol for relative protocols', function () {
-      var data = parse('//foo.com/foo', parse('http://sub.example.com:808/'));
+      var lolcation = parse('http://sub.example.com:808/')
+        , data = parse('//foo.com/foo', lolcation);
 
       assume(data.port).equals('');
       assume(data.host).equals('foo.com');
       assume(data.protocol).equals('http:');
       assume(data.href).equals('http://foo.com/foo');
@@ -527,17 +564,17 @@ describe('url-parse', function () {
     it('does not inherit pathname for non relative urls', function () {
       var data = parse('http://localhost', parse('http://foo:bar@sub.example.com/bar?foo=bar#hash'));
 
       assume(data.port).equals('');
       assume(data.host).equals('localhost');
-      assume(data.href).equals('http://localhost');
+      assume(data.href).equals('http://localhost/');
     });
 
     it('resolves pathname for relative urls', function () {
       var data, i = 0;
       var tests = [
-        ['', 'http://foo.com', ''],
+        ['', 'http://foo.com', '/'],
         ['', 'http://foo.com/', '/'],
         ['', 'http://foo.com/a', '/a'],
         ['a', 'http://foo.com', '/a'],
         ['a/', 'http://foo.com', '/a/'],
         ['b/c', 'http://foo.com/a', '/b/c'],
@@ -720,16 +757,16 @@ describe('url-parse', function () {
       var data = parse('http://example.com');
 
       data.set('hash', 'usage');
 
       assume(data.hash).equals('#usage');
-      assume(data.href).equals('http://example.com#usage');
+      assume(data.href).equals('http://example.com/#usage');
 
       data.set('hash', '#license');
 
       assume(data.hash).equals('#license');
-      assume(data.href).equals('http://example.com#license');
+      assume(data.href).equals('http://example.com/#license');
     });
 
     it('updates the port when updating host', function () {
       var data = parse('http://google.com/?foo=bar');
 

Vulnerable code:   - Fixed in: 1.4.5
  , slashes = /^[A-Za-z][A-Za-z0-9+-.]*:\/\//
  , protocolre = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\S\s]*)/i
  var match = protocolre.exec(address);
    protocol: match[1] ? match[1].toLowerCase() : '',
    slashes: !!match[2],
    assume(parsed.href).equals('http://example.com');
    parsed = parse('//\\what-is-up.com');
    assume(parsed.host).equals('');
    assume(parsed.hostname).equals('');
          href: 'http://example.com',
          pathname: ''
          protocol: 'mailto:'
          protocol: 'data:'
          protocol: 'sip:'
      var data;
        data = parse(testData[i].href);
        assume(data.protocol).equals(testData[i].protocol);
        assume(data.pathname).equals(testData[i].pathname);
      var url = 'http://user:password@[3ffe:2a00:100:7031::1]:8080'
      assume(data.href).equals('http://localhost');
      var data = parse('//foo.com/foo', parse('http://sub.example.com:808/'));
      assume(data.href).equals('http://localhost');
        ['', 'http://foo.com', ''],
      assume(data.href).equals('http://example.com#usage');
      assume(data.href).equals('http://example.com#license');
Fixed code: > Using backslash in the protocol is valid in the browser, while url-parse
> thinks it’s a relative path. An application that validates a url using
> url-parse might pass a malicious link.

- **Reporter credits**
  - CxSCA AppSec team at Checkmarx.
  - Twitter: [Yaniv Nizry](https://twitter.com/ynizry)
- Fixed in: 1.5.0

- Fixed in: 1.4.5
  , slashes = /^[A-Za-z][A-Za-z0-9+-.]*:[\\/]+/
  , protocolre = /^([a-z][a-z0-9.+-]*:)?([\\/]{1,})?([\S\s]*)/i

  var match = protocolre.exec(address)
    , protocol = match[1] ? match[1].toLowerCase() : ''
    , slashes = !!(match[2] && match[2].length >= 2);
    protocol: protocol,
    slashes: slashes,
  //
  // Default to a / for pathname if none exists. This normalizes the URL
  // to always have a /
  //
  if (url.pathname.charAt(0) !== '/' && url.hostname) {
    url.pathname = '/' + url.pathname;
  }

  spec.slashes = true;
  
    assume(parsed.pathname).equals('/');
    assume(parsed.href).equals('http://example.com/');
    parsed = parse('http://yolo.com\\what-is-up.com');
    assume(parsed.host).equals('what-is-up.com');
    assume(parsed.href).equals('//what-is-up.com/');
  });

  it('does not see a slash after the protocol as path', function () {
    var url = 'https:\\/github.com/foo/bar'
      , parsed = parse(url);

    assume(parsed.host).equals('github.com');
    assume(parsed.hostname).equals('github.com');
    assume(parsed.pathname).equals('/foo/bar');

    url = 'https:/\/\/\github.com/foo/bar';
    assume(parsed.host).equals('github.com');
    assume(parsed.hostname).equals('github.com');
    assume(parsed.pathname).equals('/foo/bar');
          href: 'http://example.com/',
          pathname: '/',
          slashes: true
        },
        {
          href: 'ws://example.com/',
          protocol: 'ws:',
          pathname: '/',
          slashes: true
        },
        {
          href: 'wss://example.com/',
          protocol: 'wss:',
          pathname: '/',
          slashes: true
          protocol: 'mailto:',
          slashes: false
          protocol: 'data:',
          slashes: false,
          protocol: 'sip:',
          slashes: false,
      var data, test;
        test = testData[i];
        data = parse(test.href);

        assume(data.protocol).equals(test.protocol);
        assume(data.pathname).equals(test.pathname);
        assume(data.slashes).equals(test.slashes);
        assume(data.href).equals(test.href);
      var url = 'http://user:password@[3ffe:2a00:100:7031::1]:8080/'
      assume(parsed.pathname).equals('/');
      assume(data.href).equals('http://localhost/');
      var lolcation = parse('http://sub.example.com:808/')
        , data = parse('//foo.com/foo', lolcation);
      assume(data.href).equals('http://localhost/');
        ['', 'http://foo.com', '/'],
      assume(data.href).equals('http://example.com/#usage');
      assume(data.href).equals('http://example.com/#license');