← WebKit — 2026-W35
c8fdf5eb819d6b75b99ab2288ce2887d33326d27 Descendant and dynamic module script fetches send the wrong Referer header
severity low
class InfoDisclosure
confidence 0.45
signal score 10.17
Source/JavaScriptCore/runtime
Primitive: importing module script URL leaked as Referer to non-HTTP scheme handlers
Triage note: Threads a module-fetch referrer through JSModuleLoader/ScriptModuleLoader/CachedScriptFetcher and only sets HTTP Referrer when request.url().protocolIsInHTTPFamily(), with a comment that otherwise the importing script's URL would be sent unfiltered e.g. to a URL scheme handler. The guard is a genuine referrer-leak fix, but most of the diff is referrer plumbing/feature work.
Triage note: Threads a module-fetch referrer through JSModuleLoader/ScriptModuleLoader/CachedScriptFetcher and only sets HTTP Referrer when request.url().protocolIsInHTTPFamily(), with a comment that otherwise the importing script's URL would be sent unfiltered e.g. to a URL scheme handler. The guard is a genuine referrer-leak fix, but most of the diff is referrer plumbing/feature work.
Structural signals: smartptr_member_added (+6.47), null_guard (+1.2), area:Source/JavaScriptCore/runtime (+2.5)
Commit message
Descendant and dynamic module script fetches send the wrong Referer header
https://bugs.webkit.org/show_bug.cgi?id=322392
Reviewed by Yusuke Suzuki and Ryosuke Niwa.
When fetching a module script, we never set a referrer on the request, so the load fell back to the
fetch client's outgoing referrer: the document URL for documents, and the global scope URL for
workers and worklets (which, for a threaded worklet, is the URL of the window that created it).
Per HTML, the referrer of a module fetch is the referring script's base URL: "fetch the descendants
of a module script" passes "module script's base URL" as the referrer for each descendant, and
HostLoadImportedModule likewise uses the referencing script's base URL for dynamic import(). Only a
fetch with no referring script — a script element or worker top-level fetch — uses the fetch client's
referrer. Both Blink and Gecko implement this.
The referrer was not available where the fetch happens, because JSC's moduleLoaderFetch hook only
receives the module key. Pass the referring script's base URL through the hook as a String:
JSModuleLoader::hostLoadImportedModule already resolves the specifier against the referring module's
key, and requestImportModule() has the importing script's base URL for dynamic import(). Both now
forward it to JSModuleLoader::fetch(), which hands it to the host.
ScriptModuleLoader::fetch() maps a module key through responseURLFromRequestURL(), so a redirected
importer contributes its response URL. A dynamic import's base URL is already a response URL and is
not a key in that map, so it comes back unchanged. An inline module script has no URL of its own, its
key being a Symbol, so the hook passes the empty string and ScriptModuleLoader substitutes the
script's base URL — the same URL resolve() resolves its specifiers against, and not necessarily the
document URL, since a <base> element can move it. A null referrer means there is no referring script
and keeps the previous behavior of falling back to the fetch client's outgoing referrer.
The two loaders apply it differently. WorkerModuleScriptLoader passes it as the ThreadableLoader
referrer rather than setting it on the ResourceRequest, since referrer and origin headers are only
set after a preflight and DocumentThreadableLoader asserts the request carries neither.
CachedScriptFetcher sets it on the request directly, but only for an HTTP(S) URL, because
CachedResourceLoader::requestResource() calls updateHTTPRequestHeaders() only for those: on any other
scheme the referrer would never have the referrer policy applied to it and would be sent unfiltered,
e.g. to a URL scheme handler, even under Referrer-Policy: no-referrer. For an HTTP(S) request,
CachedResourceRequest::updateReferrerAndOriginHeaders() prefers the pre-set referrer and applies the
referrer policy to it.
Tests: imported/w3c/web-platform-tests/html/semantics/scripting-1/the-script-element/module/module-import-referrer-base-url.html
imported/w3c/web-platform-tests/worklets/audio-worklet-referrer.https.html
imported/w3c/web-platform-tests/html/semantics/scripting-1/the-script-element/module/module-import-referrer.html
imported/w3c/web-platform-tests/html/semantics/scripting-1/the-script-element/module/referrer-strict-policies.sub.html
URLSchemeHandler.ModuleDescendantFetchDoesNotSendReferrer
* LayoutTests/imported/w3c/web-platform-tests/html/semantics/scripting-1/the-script-element/module/module-import-referrer-base-url-expected.txt: Added.
* LayoutTests/imported/w3c/web-platform-tests/html/semantics/scripting-1/the-script-element/module/module-import-referrer-base-url.html: Added.
* LayoutTests/imported/w3c/web-platform-tests/html/semantics/scripting-1/the-script-element/module/module-import-referrer-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/html/semantics/scripting-1/the-script-element/module/referrer-strict-policies.sub-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/worklets/audio-worklet-referrer.https-expected.txt:
* Source/JavaScriptCore/API/JSAPIGlobalObject.h:
* Source/JavaScriptCore/API/JSAPIGlobalObject.mm:
(JSC::JSAPIGlobalObject::moduleLoaderFetch):
* Source/JavaScriptCore/jsc.cpp:
(GlobalObject::moduleLoaderFetch):
* Source/JavaScriptCore/runtime/GlobalObjectMethodTable.h:
* Source/JavaScriptCore/runtime/JSModuleLoader.cpp:
(JSC::moduleReferrer):
(JSC::JSModuleLoader::loadModule):
(JSC::JSModuleLoader::requestImportModule):
(JSC::JSModuleLoader::fetch):
(JSC::JSModuleLoader::hostLoadImportedModule):
* Source/JavaScriptCore/runtime/JSModuleLoader.h:
* Source/WebCore/bindings/js/CachedModuleScriptLoader.cpp:
(WebCore::CachedModuleScriptLoader::load):
* Source/WebCore/bindings/js/CachedModuleScriptLoader.h:
* Source/WebCore/bindings/js/CachedScriptFetcher.cpp:
(WebCore::CachedScriptFetcher::requestModuleScript const):
(WebCore::CachedScriptFetcher::requestScriptWithCache const):
* Source/WebCore/bindings/js/CachedScriptFetcher.h:
(WebCore::CachedScriptFetcher::requestScriptWithCache):
* Source/WebCore/bindings/js/JSDOMGlobalObject.cpp:
(WebCore::JSDOMGlobalObject::moduleLoaderFetch):
* Source/WebCore/bindings/js/JSDOMGlobalObject.h:
* Source/WebCore/bindings/js/ScriptModuleLoader.cpp:
(WebCore::ScriptModuleLoader::fetch):
(WebCore::ScriptModuleLoader::responseURLFromRequestURL):
(WebCore::ScriptModuleLoader::baseURLForScriptWithoutURL):
* Source/WebCore/bindings/js/ScriptModuleLoader.h:
* Source/WebCore/bindings/js/WorkerModuleScriptLoader.cpp:
(WebCore::WorkerModuleScriptLoader::load):
* Source/WebCore/bindings/js/WorkerModuleScriptLoader.h:
* Source/WebCore/dom/ScriptElementCachedScriptFetcher.cpp:
(WebCore::ScriptElementCachedScriptFetcher::requestModuleScript const):
* Source/WebCore/dom/ScriptElementCachedScriptFetcher.h:
* Source/WebCore/workers/WorkerScriptLoader.cpp:
(WebCore::WorkerScriptLoader::loadAsynchronously):
* Source/WebCore/workers/WorkerScriptLoader.h:
* Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKURLSchemeHandler-1.mm:
((URLSchemeHandler, ModuleDescendantFetchDoesNotSendReferrer)):
Canonical link: https://commits.webkit.org/319921@main
Before / after
Loading diff…
On This Page