From abd49f123fafd7073cee1cf711d7ee15bd18effc Mon Sep 17 00:00:00 2001 From: hikmetba-bit Date: Sat, 19 Sep 2026 16:48:08 +0300 Subject: [PATCH] Don't throw on malformed percent-encoding in userinfo The constructor and set('auth', ...) called encodeURIComponent( decodeURIComponent(x)) on username/password with no guard, so a stray '%' (or any invalid percent-escape) in the userinfo propagated a raw URIError out of new Url(...) / set(...). This contradicts url-parse's documented tolerant-parser design (#195) and diverges from the WHATWG URL parser, which accepts the same input without throwing. Add a decode() helper that falls back to the raw (still-encoded) input on URIError, and use it at all six username/password call sites in the constructor and set('auth'). Invalid input still round-trips safely (re-encoded rather than decoded), it just no longer throws. Fixes #242. Co-Authored-By: Claude Sonnet 5 --- index.js | 30 ++++++++++++++++++++++++------ test/test.js | 17 +++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index bb42723..c3319c8 100644 --- a/index.js +++ b/index.js @@ -148,6 +148,24 @@ function isSpecial(scheme) { ); } +/** + * Decode a URI component without throwing on malformed percent-encoding. + * url-parse is documented as a tolerant parser (see #195), so a stray `%` + * in the userinfo must not raise; the WHATWG URL parser tolerates the same + * input. + * + * @param {String} input Value to decode. + * @return {String} Decoded value, or the original input if decoding fails. + * @private + */ +function decode(input) { + try { + return decodeURIComponent(input); + } catch (e) { + return input; + } +} + /** * @typedef ProtocolExtract * @type Object @@ -410,12 +428,12 @@ function Url(address, location, parser) { if (~index) { url.username = url.auth.slice(0, index); - url.username = encodeURIComponent(decodeURIComponent(url.username)); + url.username = encodeURIComponent(decode(url.username)); url.password = url.auth.slice(index + 1); - url.password = encodeURIComponent(decodeURIComponent(url.password)) + url.password = encodeURIComponent(decode(url.password)) } else { - url.username = encodeURIComponent(decodeURIComponent(url.auth)); + url.username = encodeURIComponent(decode(url.auth)); } url.auth = url.password ? url.username +':'+ url.password : url.username; @@ -525,12 +543,12 @@ function set(part, value, fn) { if (~index) { url.username = value.slice(0, index); - url.username = encodeURIComponent(decodeURIComponent(url.username)); + url.username = encodeURIComponent(decode(url.username)); url.password = value.slice(index + 1); - url.password = encodeURIComponent(decodeURIComponent(url.password)); + url.password = encodeURIComponent(decode(url.password)); } else { - url.username = encodeURIComponent(decodeURIComponent(value)); + url.username = encodeURIComponent(decode(value)); } } diff --git a/test/test.js b/test/test.js index 1cea881..8ca3333 100644 --- a/test/test.js +++ b/test/test.js @@ -831,6 +831,23 @@ describe('url-parse', function () { assume(parsed.href).equals('http://user%40:pas%3As%40@www.example.com/'); }); + it('does not throw on malformed percent-encoding in username/password', function () { + // decodeURIComponent('%') throws; the fallback keeps the raw value and + // re-encodes it, so it round-trips as a literal '%' (i.e. '%25') rather + // than propagating the URIError. + assume(function () { parse('http://%@example.com/'); }).does.not.throw(); + assume(parse('http://%@example.com/').username).equals('%25'); + assume(parse('http://%@example.com/').hostname).equals('example.com'); + + assume(function () { parse('http://user:%@example.com/'); }).does.not.throw(); + assume(parse('http://user:%@example.com/').password).equals('%25'); + + assume(function () { + parse('http://example.com/').set('auth', '%'); + }).does.not.throw(); + assume(parse('http://example.com/').set('auth', '%').username).equals('%25'); + }); + it('adds @ to href if auth and host are empty', function () { var parsed, i = 0; var urls = [