From 79d250dc5963223f55d0e06b40a4322555846ae9 Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Tue, 6 Oct 2026 21:59:54 +0300 Subject: [PATCH 1/2] fix: stop URL auth at query and fragment delimiters --- src/parse.ts | 2 +- test/parse.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/parse.ts b/src/parse.ts index b2a2e7e0..9e0cd99e 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -72,7 +72,7 @@ export function parseURL(input = "", defaultProto?: string): ParsedURL { const [, protocol = "", auth, hostAndPath = ""] = input .replace(/\\/g, "/") - .match(/^[\s\0]*([\w+.-]{2,}:)?\/\/([^/@]+@)?(.*)/) || []; + .match(/^[\s\0]*([\w+.-]{2,}:)?\/\/([^/?#@]+@)?(.*)/) || []; // eslint-disable-next-line prefer-const let [, host = "", path = ""] = hostAndPath.match(/([^#/?]*)(.*)?/) || []; diff --git a/test/parse.test.ts b/test/parse.test.ts index ba72667a..bf5270eb 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -294,3 +294,43 @@ describe("parseFilename", () => { }); } }); + +describe("parseURL authority boundaries", () => { + test.each([ + [ + "http://example.com?email=user@example.org", + "?email=user@example.org", + "", + ], + ["https://example.com#user@example.org", "", "#user@example.org"], + [ + "//example.com?email=user@example.org#fragment", + "?email=user@example.org", + "#fragment", + ], + ])("keeps @ after the authority boundary in %s", (input, search, hash) => { + expect(parseURL(input)).toMatchObject({ + protocol: input.startsWith("//") ? "" : input.split("//")[0], + auth: "", + host: "example.com", + pathname: "", + search, + hash, + }); + }); + + test("preserves actual user info before query and fragment", () => { + expect( + parseURL( + "https://user:pass@example.com?email=user@example.org#user@example.org", + ), + ).toMatchObject({ + protocol: "https:", + auth: "user:pass", + host: "example.com", + pathname: "", + search: "?email=user@example.org", + hash: "#user@example.org", + }); + }); +}); From e7fe0e55b65d9bc67cc2fee638c67de4a92a8ada Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:09:48 +0300 Subject: [PATCH 2/2] fix: use the last user-info delimiter in URL authority --- src/parse.ts | 2 +- test/parse.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/parse.ts b/src/parse.ts index 9e0cd99e..73019b76 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -72,7 +72,7 @@ export function parseURL(input = "", defaultProto?: string): ParsedURL { const [, protocol = "", auth, hostAndPath = ""] = input .replace(/\\/g, "/") - .match(/^[\s\0]*([\w+.-]{2,}:)?\/\/([^/?#@]+@)?(.*)/) || []; + .match(/^[\s\0]*([\w+.-]{2,}:)?\/\/([^/?#]*@)?(.*)/) || []; // eslint-disable-next-line prefer-const let [, host = "", path = ""] = hostAndPath.match(/([^#/?]*)(.*)?/) || []; diff --git a/test/parse.test.ts b/test/parse.test.ts index bf5270eb..986c355d 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -334,3 +334,31 @@ describe("parseURL authority boundaries", () => { }); }); }); + +describe("parseURL user-info separator", () => { + test.each([ + ["https://user@team@example.com", "user@team", "", ""], + [ + "https://user@team@example.com?email=user@example.org", + "user@team", + "?email=user@example.org", + "", + ], + [ + "https://user@team@example.com#user@example.org", + "user@team", + "", + "#user@example.org", + ], + ["https://@example.com", "", "", ""], + ])("uses the last authority @ in %s", (input, auth, search, hash) => { + expect(parseURL(input)).toMatchObject({ + protocol: "https:", + auth, + host: "example.com", + pathname: "", + search, + hash, + }); + }); +});