CVE-2021-3664 (published 2021-07-26T12:15Z)

url-parse is vulnerable to URL Redirection to Untrusted Site

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: CWE-601
cwe_name: URL Redirection to Untrusted Site ('Open Redirect')
description: A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.
extended_description: An http parameter may contain a URL value and could cause the web application to redirect the request to the specified URL. By modifying the URL value to a malicious site, an attacker may successfully launch a phishing scam and steal user credentials. Because the server name in the modified link is identical to the original site, phishing attempts have a more trustworthy appearance. Whether this issue poses a vulnerability will be subject to the intended behavior of the application. For example, a search engine might intentionally provide redirects to arbitrary URLs.
         

Fix information:
repo_url: https://github.com/unshiftio/url-parse
commit_details: repo_url: https://github.com/unshiftio/url-parse
author: Luigi Pinca
author_date: 2021-07-23 18:31:42+02:00
msg: [fix] Ignore slashes after the protocol for special URLs

Fixes #205
Fixes #206
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: index.js
change_type: ModificationType.MODIFY
diff: @@ -98,6 +98,24 @@ function lolcation(loc) {
   return finaldestination;
 }
 
+/**
+ * Check whether a protocol scheme is special.
+ *
+ * @param {String} The protocol scheme of the URL
+ * @return {Boolean} `true` if the protocol scheme is special, else `false`
+ * @private
+ */
+function isSpecial(scheme) {
+  return (
+    scheme === 'file:' ||
+    scheme === 'ftp:' ||
+    scheme === 'http:' ||
+    scheme === 'https:' ||
+    scheme === 'ws:' ||
+    scheme === 'wss:'
+  );
+}
+
 /**
  * @typedef ProtocolExtract
  * @type Object
@@ -110,16 +128,32 @@ function lolcation(loc) {
  * Extract protocol information from a URL with/without double slash ("//").
  *
  * @param {String} address URL we want to extract from.
+ * @param {Object} location
  * @return {ProtocolExtract} Extracted information.
  * @private
  */
-function extractProtocol(address) {
+function extractProtocol(address, location) {
   address = trimLeft(address);
+  location = location || {};
 
-  var match = protocolre.exec(address)
-    , protocol = match[1] ? match[1].toLowerCase() : ''
-    , slashes = !!(match[2] && match[2].length >= 2)
-    , rest =  match[2] && match[2].length === 1 ? '/' + match[3] : match[3];
+  var match = protocolre.exec(address);
+  var protocol = match[1] ? match[1].toLowerCase() : '';
+  var rest = match[2] ? match[2] + match[3] : match[3];
+  var slashes = !!(match[2] && match[2].length >= 2);
+
+  if (protocol === 'file:') {
+    if (slashes) {
+      rest = rest.slice(2);
+    }
+  } else if (isSpecial(protocol)) {
+    rest = match[3];
+  } else if (protocol) {
+    if (rest.indexOf('//') === 0) {
+      rest = rest.slice(2);
+    }
+  } else if (slashes && location.hostname) {
+    rest = match[3];
+  }
 
   return {
     protocol: protocol,
@@ -214,7 +248,7 @@ function Url(address, location, parser) {
   //
   // Extract protocol information before running the instructions.
   //
-  extracted = extractProtocol(address || '');
+  extracted = extractProtocol(address || '', location);
   relative = !extracted.protocol && !extracted.slashes;
   url.slashes = extracted.slashes || relative && location.slashes;
   url.protocol = extracted.protocol || location.protocol || '';
@@ -224,7 +258,10 @@ function Url(address, location, parser) {
   // When the authority component is absent the URL starts with a path
   // component.
   //
-  if (!extracted.slashes || url.protocol === 'file:') {
+  if (
+    url.protocol === 'file:' ||
+    (!extracted.slashes && !isSpecial(extracted.protocol))
+  ) {
     instructions[3] = [/(.*)/, 'pathname'];
   }
 

programming_language: JavaScript
filename: test.js
change_type: ModificationType.MODIFY
diff: @@ -93,7 +93,7 @@ describe('url-parse', function () {
       assume(parse.extractProtocol('//foo/bar')).eql({
         slashes: true,
         protocol: '',
-        rest: 'foo/bar'
+        rest: '//foo/bar'
       });
     });
 
@@ -283,7 +283,7 @@ describe('url-parse', function () {
     assume(parsed.href).equals('http://what-is-up.com/');
   });
 
-  it('does not see a slash after the protocol as path', function () {
+  it('ignores slashes after the protocol for special URLs', function () {
     var url = 'https:\\/github.com/foo/bar'
       , parsed = parse(url);
 
@@ -292,11 +292,59 @@ describe('url-parse', function () {
     assume(parsed.pathname).equals('/foo/bar');
 
     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';
+    parsed = parse(url);
+    assume(parsed.host).equals('github.com');
+    assume(parsed.pathname).equals('/foo/bar');
+
+    url = 'https:\\github.com/foo/bar';
+    parsed = parse(url);
+    assume(parsed.host).equals('github.com');
+    assume(parsed.pathname).equals('/foo/bar');
+
+    url = 'https:github.com/foo/bar';
+    parsed = parse(url);
+    assume(parsed.host).equals('github.com');
+    assume(parsed.pathname).equals('/foo/bar');
+
+    url = 'https:github.com/foo/bar';
+    parsed = parse(url);
+    assume(parsed.host).equals('github.com');
+    assume(parsed.pathname).equals('/foo/bar');
   });
 
+  it('handles slashes after the protocol for non special URLs', function () {
+    var url = 'foo:example.com'
+      , parsed = parse(url);
+
+    assume(parsed.hostname).equals('');
+    assume(parsed.pathname).equals('example.com');
+    assume(parsed.href).equals('foo:example.com');
+
+    url = 'foo:/example.com';
+    parsed = parse(url);
+    assume(parsed.hostname).equals('');
+    assume(parsed.pathname).equals('/example.com');
+    assume(parsed.href).equals('foo:/example.com');
+
+    url = 'foo://example.com';
+    parsed = parse(url);
+    assume(parsed.hostname).equals('example.com');
+    assume(parsed.pathname).equals('/');
+    assume(parsed.href).equals('foo://example.com/');
+
+    url = 'foo:///example.com';
+    parsed = parse(url);
+    assume(parsed.hostname).equals('');
+    assume(parsed.pathname).equals('/example.com');
+    assume(parsed.href).equals('foo:///example.com');
+  })
+
   describe('origin', function () {
     it('generates an origin property', function () {
       var url = 'http://google.com:80/pathname'
@@ -440,7 +488,7 @@ describe('url-parse', function () {
     });
 
     it('handles the file: protocol', function () {
-      var slashes = ['', '/', '//', '///', '////', '/////'];
+      var slashes = ['', '/', '//', '///'];
       var data;
       var url;
 
@@ -451,6 +499,18 @@ describe('url-parse', function () {
         assume(data.href).equals('file:///');
       }
 
+      url = 'file:////';
+      data = parse(url);
+      assume(data.protocol).equals('file:');
+      assume(data.pathname).equals('//');
+      assume(data.href).equals(url);
+
+      url = 'file://///';
+      data = parse(url);
+      assume(data.protocol).equals('file:');
+      assume(data.pathname).equals('///');
+      assume(data.href).equals(url);
+
       url = 'file:///Users/foo/BAR/baz.pdf';
       data = parse(url);
       assume(data.protocol).equals('file:');

programming_language: JavaScript