CVE-2021-27515 (published 2021-02-22T00:15Z)

url-parse before 1.5.0 mishandles certain uses of backslash such as http:\\/ and interprets the URI as a relative path.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/unshiftio/url-parse
commit_details: repo_url: https://github.com/unshiftio/url-parse
author: Arnout Kazemier
author_date: 2021-02-17 16:17:44+01:00
msg: [security] More backslash fixes (#197)
repository: repo_url: https://github.com/unshiftio/url-parse
repo_name: unshiftio/url-parse
description: Small footprint URL parser that works seamlessly across Node.js and browser environments.
repo_language: JavaScript
owner: unshiftio
file_changes: filename: SECURITY.md
change_type: ModificationType.MODIFY
diff: @@ -33,13 +33,22 @@ 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
 
 ---
 

programming_language: Markdown
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -2,8 +2,8 @@
 
 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 +'+');
 
@@ -115,11 +115,14 @@ function lolcation(loc) {
  */
 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]
   };
 }
@@ -280,6 +283,14 @@ function Url(address, location, parser) {
     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

programming_language: JavaScript
filename: fuzzy.js
change_type: ModificationType.MODIFY
diff: @@ -103,6 +103,8 @@ module.exports = function generate() {
     , key;
 
   spec.protocol = get('protocol');
+  spec.slashes = true;
+  
   spec.hostname = get('hostname');
   spec.pathname = get('pathname');
 

programming_language: TypeScript
filename: test.js
change_type: ModificationType.MODIFY
diff: @@ -190,9 +190,10 @@ describe('url-parse', function () {
       , 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 () {
@@ -242,7 +243,7 @@ describe('url-parse', function () {
     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');
   });
 
@@ -250,8 +251,22 @@ describe('url-parse', 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 () {
@@ -327,32 +342,52 @@ describe('url-parse', 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);
       }
     });
 
@@ -391,13 +426,14 @@ describe('url-parse', function () {
     });
 
     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);
     });
 
@@ -467,7 +503,7 @@ describe('url-parse', function () {
 
       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 () {
@@ -516,7 +552,8 @@ describe('url-parse', function () {
     });
 
     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');
@@ -529,13 +566,13 @@ describe('url-parse', function () {
 
       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'],
@@ -722,12 +759,12 @@ describe('url-parse', function () {
       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 () {

programming_language: JavaScript