Open Redirect in unshiftio/url-parse
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.
| Package | Affected versions | Patched versions |
|---|---|---|
url-parsenpm | >= 0.1.0, < 1.5.2 | 1.5.2 |
Affected products
2- unshiftio/unshiftio/url-parsev5Range: unspecified
Patches
181ab967889b0[fix] Ignore slashes after the protocol for special URLs
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- github.com/advisories/GHSA-hh27-ffr2-f2jcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-3664ghsaADVISORY
- github.com/github/advisory-database/pull/6764ghsaWEB
- github.com/unshiftio/url-parse/commit/81ab967889b08112d3356e451bf03e6aa0cbb7e0ghsaWEB
- github.com/unshiftio/url-parse/issues/205ghsaWEB
- github.com/unshiftio/url-parse/issues/206ghsaWEB
- huntr.dev/bounties/1625557993985-unshiftio/url-parseghsaWEB
- lists.debian.org/debian-lts-announce/2023/02/msg00030.htmlghsamailing-listWEB
News mentions
0No linked articles in our index yet.