From f14a82cecdd18e2cc1563a37ab197e87b57efef0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 17 Aug 2026 03:51:22 +0000 Subject: [PATCH] url: give blob: URLs a null origin when the inner host has invalid punycode URLDecomposition::origin() re-parses a blob: URL's path with WTF::URL alone. Since the WebKit bump that removed the parser's own xn-- check, that parse accepts hosts such as xn--a.com that the URL constructor, URL.parse, URL.canParse and the host setters reject through hasValidPunycodeHost, so new URL("blob:http://xn--a.com/").origin returned "http://xn--a.com" while new URL("http://xn--a.com/") threw. Apply the same host check to the inner URL, so it counts as unparsed and the origin is "null", matching Node and Bun 1.3. --- src/jsc/bindings/URLDecomposition.cpp | 2 +- test/js/web/url/url.test.ts | 34 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/jsc/bindings/URLDecomposition.cpp b/src/jsc/bindings/URLDecomposition.cpp index a59c4747e242..1dbae0f4c252 100644 --- a/src/jsc/bindings/URLDecomposition.cpp +++ b/src/jsc/bindings/URLDecomposition.cpp @@ -47,7 +47,7 @@ String URLDecomposition::origin() const if (fullURL.protocolIsBlob()) { const String& path = fullURL.path().toString(); const URL subUrl { URL {}, path }; - if (subUrl.isValid()) { + if (subUrl.isValid() && hasAcceptableHost(subUrl)) { if (subUrl.protocolIsInHTTPFamily() or subUrl.protocolIsInFTPFamily() or subUrl.protocolIs("ws"_s) or subUrl.protocolIs("wss"_s) or subUrl.protocolIsFile()) return subUrl.protocolHostAndPort(); } diff --git a/test/js/web/url/url.test.ts b/test/js/web/url/url.test.ts index 41ba8ec9ee37..1d3857dbc27d 100755 --- a/test/js/web/url/url.test.ts +++ b/test/js/web/url/url.test.ts @@ -250,6 +250,40 @@ describe("url", () => { expect(v.href).toBe("http://example.com/"); }); + it("blob: origin is null when the inner URL has an invalid punycode label (like Node)", () => { + // The origin of a blob: URL is the origin of its re-parsed path, so an + // inner URL the constructor rejects must not produce a tuple origin. + for (const inner of [ + "http://xn--a.com/", + "https://XN--A.com/x", + "http://xn--a/", + "http://x%6E--a.com/", + "http://xn--nxasmq6b.xn--a.com/", + "ws://xn--a-.com/", + "wss://xn---.com/", + "ftp://xn--a.com/", + "file://xn--a/x", + ]) { + expect(() => new URL(inner)).toThrow(TypeError); + expect(new URL("blob:" + inner).origin).toBe("null"); + } + expect( + [ + "http://xn--ls8h.la/", + "https://xn--ls8h.la:8443/x", + "http://m\u00FCnchen.de/x", + "http://xn--ls8h.la/xn--a", + "http://ok.example/?xn--a", + ].map(inner => new URL("blob:" + inner).origin), + ).toEqual([ + "http://xn--ls8h.la", + "https://xn--ls8h.la:8443", + "http://xn--mnchen-3ya.de", + "http://xn--ls8h.la", + "http://ok.example", + ]); + }); + it("prints", () => { // URL.prototype carries [Symbol.for("nodejs.util.inspect.custom")], so // Bun.inspect matches node's util.inspect output.