VYPR
Moderate severityNVD Advisory· Published Jul 26, 2021· Updated Aug 3, 2024

Open Redirect in unshiftio/url-parse

CVE-2021-3664

Description

url-parse is vulnerable to URL Redirection to Untrusted Site

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

url-parse library vulnerable to open redirect and hostname spoofing due to improper handling of slashes after the protocol.

Vulnerability

The url-parse library prior to the fix (commit 81ab967) misparses URLs with backslashes or extra slashes after the protocol, such as https:\\/github.com or https:/github.com, causing the hostname to be misinterpreted. This can lead to open redirect and hostname spoofing [2][4].

Exploitation

An attacker can craft a URL with a manipulated protocol/slash sequence, e.g., https:\\attacker.com, which url-parse incorrectly parses as having host attacker.com instead of the intended domain. The attacker can trick users into clicking such a link, believing it leads to a legitimate site, while it redirects to an untrusted site [2].

Impact

Successful exploitation allows an attacker to redirect users to arbitrary malicious websites (open redirect) or spoof hostnames, aiding phishing attacks and theft of credentials [2][3].

Mitigation

The vulnerability is fixed in commit 81ab967 [4]. Users should upgrade to the latest version of url-parse that includes this fix [1][3]. No workaround is available; updating is the recommended action.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
url-parsenpm
>= 0.1.0, < 1.5.21.5.2

Affected products

2
  • ghsa-coords
    Range: >= 0.1.0, < 1.5.2
  • unshiftio/unshiftio/url-parsev5
    Range: unspecified

Patches

1
81ab967889b0

[fix] Ignore slashes after the protocol for special URLs

https://github.com/unshiftio/url-parseLuigi PincaJul 23, 2021via ghsa
2 files changed · +107 10
  • index.js+44 7 modified
    @@ -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'];
       }
     
    
  • test/test.js+63 3 modified
    @@ -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:');
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.