From aa738bc86388170a0cdce63be71395111cdd153a Mon Sep 17 00:00:00 2001 From: aashish00021 Date: Fri, 31 Jul 2026 15:43:45 -0400 Subject: [PATCH] fix: skip non-http(s) links in anchor click handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handleAnchor only rejected links by origin, but `blob:` URLs inherit the page origin and slipped through, getting routed to a garbage pathname. Guard against any non-http(s) scheme (blob:, mailto:, tel:, data:, …). Fixes #382 --- .changeset/skip-non-http-anchor-links.md | 5 ++++ src/data/events.ts | 3 +++ test/data/events.spec.ts | 34 +++++++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 .changeset/skip-non-http-anchor-links.md diff --git a/.changeset/skip-non-http-anchor-links.md b/.changeset/skip-non-http-anchor-links.md new file mode 100644 index 00000000..80f86894 --- /dev/null +++ b/.changeset/skip-non-http-anchor-links.md @@ -0,0 +1,5 @@ +--- +"@solidjs/router": patch +--- + +Anchor click handling no longer intercepts links with a non-http(s) scheme (`blob:`, `mailto:`, `tel:`, `data:`, …). Previously `blob:` links could be wrongly routed because they inherit the page origin and bypassed the same-origin check. diff --git a/src/data/events.ts b/src/data/events.ts index 82c509a8..85382142 100644 --- a/src/data/events.ts +++ b/src/data/events.ts @@ -56,6 +56,9 @@ export function setupNativeEvents({ if (a.hasAttribute("download") || (rel && rel.includes("external"))) return; const url = svg ? new URL(href, document.baseURI) : new URL(href); + // Skip non-http(s) schemes (blob:, mailto:, tel:, data:, ...). blob: URLs + // inherit the page origin, so the origin check below won't reject them. #382 + if (url.protocol !== "https:" && url.protocol !== "http:") return; if ( url.origin !== window.location.origin || (basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) diff --git a/test/data/events.spec.ts b/test/data/events.spec.ts index 6ddbeae2..005e4df8 100644 --- a/test/data/events.spec.ts +++ b/test/data/events.spec.ts @@ -184,23 +184,35 @@ describe("anchor link handling", () => { } as any; global.URL = class MockURL { + protocol: string; origin: string; pathname: string; search: string; hash: string; constructor(url: string) { - if (url.startsWith("/")) { + if (url.startsWith("blob:")) { + // blob: URLs inherit the page origin, so the origin check alone + // would let them through — they must be rejected by protocol. #382 + this.protocol = "blob:"; + this.origin = "https://example.com"; + this.pathname = url; + this.search = ""; + this.hash = ""; + } else if (url.startsWith("/")) { + this.protocol = "https:"; this.origin = "https://example.com"; this.pathname = url; this.search = ""; this.hash = ""; } else if (url.startsWith("https://example.com")) { + this.protocol = "https:"; this.origin = "https://example.com"; this.pathname = url.replace("https://example.com", "") || "/"; this.search = ""; this.hash = ""; } else { + this.protocol = "https:"; this.origin = "https://other.com"; this.pathname = "/"; this.search = ""; @@ -249,6 +261,26 @@ describe("anchor link handling", () => { }); }); + test("should ignore non-http(s) links like blob: (#382)", () => { + return createRoot(() => { + const navigateFromRoute = vi.fn(); + mockRouter.navigatorFactory = () => navigateFromRoute; + // With no base path, a blob: link shares the page origin and its pathname + // isn't caught by the path-prefix check — so it must be rejected by + // protocol, else the router routes a garbage pathname (#382). + mockRouter.base = { path: () => "" } as any; + setupNativeEvents()(mockRouter); + + const link = createMockElement("a", { href: "blob:https://example.com/abc" }); + const event = createMockEvent("click", link, { path: [link] }); + + clickHandler(event); + + expect(event.preventDefault).not.toHaveBeenCalled(); + expect(navigateFromRoute).not.toHaveBeenCalled(); + }); + }); + test("should ignore clicks with modifier keys", () => { return createRoot(() => { setupNativeEvents()(mockRouter);