CVE-2025-4664
Overview
Background
- HTTP `Link` header
- a response header that can declare relationships such as
rel=preload,rel=modulepreload,rel=prefetch,rel=dns-prefetch,rel=preconnect, andrel=compression-dictionary, each of which can cause the browser to initiate further network activity. - Subresource response
- a response for a resource the page loads (image, script, stylesheet, etc.) as opposed to the main document response; its
Linkheader can chain additional preloads. - `PreloadHelper` link processing
- Blink’s
preload_helper.cclogic (third_party/blink/renderer/core/loader/preload_helper.cc) that walksLinkheaders and decides whether each one may trigger a network hint, a resource load, or a compression-dictionary load. - `kRestrictLinkHeaderOnSubresource`
- the new
base::Feature(disabled by default) with per-categoryBASE_FEATURE_PARAMsub-flags used to selectively disableLink-header-driven preloading on subresource responses.
Root Cause Analysis
Chrome applied Link-header-driven preloading (network hints, resource loads, and compression-dictionary loads) to subresource responses with the same permissiveness as document responses, without policy enforcement that accounts for a subresource — including a cross-origin one — being the source of the header. In the header-processing loop of preload_helper.cc, IsNetworkHintAllowed, IsResourceLoadAllowed, and IsCompressionDictionaryLoadAllowed decided allowance purely from the LoadLinksFromHeaderMode without knowing whether the header came from a subresource, so a cross-origin subresource response could initiate chained preloads.
The patch adds an is_header_on_subresource parameter to each of these helpers and an early return false gated on the corresponding sub-flag, and adds a loop guard that continues past a header when is_subresource_load && !from_same_origin && kRestrictLinkHeaderOnSubresourceCrossOrigin.Get(). This tightens behavior by giving the browser a switch to suppress subresource Link-header preloading per category (network hint, resource load, compression dictionary, and cross-origin generally). Because the new features are FEATURE_DISABLED_BY_DEFAULT, the change ships the enforcement machinery and experiment controls rather than an unconditional block; the pre-existing referrerpolicy: "no-referrer" comment referencing the same bug shows the code path was already recognized as security-sensitive.
Link header — including cross-origin subresources — as trusted to drive the same chained preloading as a first-party document, and the fix threads an is_subresource_load/is_header_on_subresource signal into each allowance check plus a cross-origin continue guard so those preloads can be policy-restricted.Attack Path
- Serve a crafted subresource
An attacker controls a subresource fetched by a victim page (for example a cross-origin resource) and attaches an HTTP
Linkheader declaring a preload/prefetch/network-hint/compression-dictionary relationship. - Header is processed for the subresource
The
preload_helper.ccloop runs theLinkheader withis_subresource_loadtrue, and pre-patchIsNetworkHintAllowed/IsResourceLoadAllowed/IsCompressionDictionaryLoadAllowedallowed it without any subresource- or cross-origin-specific restriction. - Chained load is initiated
The subresource-supplied
Linkheader causes the browser to initiate a further network request (network hint, resource load, or dictionary load) from the subresource response context. - Policy bypass / information exposure The chained load lets the attacker obtain behavior or information that the loader’s policy should have withheld from a cross-origin subresource, i.e. “insufficient policy enforcement in Loader.” (inferred specific disclosure primitive)
Impact Assessment
Link header to drive chained preloading in the victim’s renderer loading path, constituting the insufficient policy enforcement in the Loader that the advisory describes (reported via an X post from @slonser_). The precondition is that the victim page fetches an attacker-influenced subresource whose response headers the attacker controls. The vendor advisory rates this high severity and reports that it is exploited in the wild; NVD scores it CVSS 4.3. Any concrete cross-origin data-disclosure primitive beyond what the patch’s restriction flags imply is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
switchthird_party/blink/renderer/core/loader/preload_helper.cc |
modified |
Files Changed
third_party/blink/common/features.ccthird_party/blink/public/common/features.hthird_party/blink/renderer/core/loader/preload_helper.cc
Audit Directions
- Header-driven side effects from subresourcesAudit any response header (Link, CSP-report, Refresh, etc.) that can trigger further loads and verify the handler distinguishes document from subresource origin and enforces cross-origin policy.
- Origin-blind allowance predicatesLook for allow/deny helpers that decide purely on a mode/enum without a caller-origin or same-origin parameter, since they can silently grant privileged behavior to cross-origin inputs.
- Chained preload/prefetch surfacesReview
preload,modulepreload,prefetch,dns-prefetch,preconnect, andcompression-dictionaryinitiation paths for whether cross-origin subresources can initiate them and whether referrer/policy defaults are applied.
Patch
From 1ea5b9417cd993d0ddb15fec186fed016198050c Mon Sep 17 00:00:00 2001
From: Takashi Nakayama <tnak@chromium.org>
Date: Tue, 09 Dec 2025 22:48:32 -0800
Subject: [PATCH] Add features to restrict Link headers on subresource responses
This CL implements a feature flag to partially disable HTTP Link headers
on subresource responses for experiments. The prepared options are based
on a proposal doc [1].
[1]
https://docs.google.com/document/d/1OeqpA9JoCXrgIMpq-ujLuZF1tG9MNF5SA0zTuIFtZis/edit?usp=sharing
Bug: 415810136
Change-Id: I473313c350d3acf1c2a5808c551152a47e69ac21
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7172079
Commit-Queue: Takashi Nakayama <tnak@chromium.org>
Reviewed-by: Hiroshige Hayashizaki <hiroshige@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1556546}
---
diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc
index 311a725..03ce59c 100644
--- a/third_party/blink/common/features.cc
+++ b/third_party/blink/common/features.cc
@@ -2084,6 +2084,29 @@
BASE_FEATURE(kResourceFetcherStoresStrongReferences,
base::FEATURE_DISABLED_BY_DEFAULT);
+BASE_FEATURE(kRestrictLinkHeaderOnSubresource,
+ base::FEATURE_DISABLED_BY_DEFAULT);
+BASE_FEATURE_PARAM(bool,
+ kRestrictLinkHeaderOnSubresourceCompressionDictionary,
+ &kRestrictLinkHeaderOnSubresource,
+ "disable_compression_dictionary",
+ false);
+BASE_FEATURE_PARAM(bool,
+ kRestrictLinkHeaderOnSubresourceCrossOrigin,
+ &kRestrictLinkHeaderOnSubresource,
+ "disable_cross_origin",
+ false);
+BASE_FEATURE_PARAM(bool,
+ kRestrictLinkHeaderOnSubresourceNetworkHint,
+ &kRestrictLinkHeaderOnSubresource,
+ "disable_network_hint",
+ false);
+BASE_FEATURE_PARAM(bool,
+ kRestrictLinkHeaderOnSubresourceResourceLoad,
+ &kRestrictLinkHeaderOnSubresource,
+ "disable_resource_load",
+ false);
+
BASE_FEATURE(kRestrictSpellingAndGrammarHighlights,
base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE_PARAM(bool,
diff --git a/third_party/blink/public/common/features.h b/third_party/blink/public/common/features.h
index d3f5e38..07085cd 100644
--- a/third_party/blink/public/common/features.h
+++ b/third_party/blink/public/common/features.h
@@ -1616,6 +1616,27 @@
BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(
kResourceFetcherStoresStrongReferences);
+// Aggregated flag for the restriction on HTTP Link headers on subresource
+// responses. See crbug.com/417529151 for details.
+BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kRestrictLinkHeaderOnSubresource);
+// Disables only "rel=compression-dictionary".
+BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE_PARAM(
+ bool,
+ kRestrictLinkHeaderOnSubresourceCompressionDictionary);
+// Disables all types of chained-preloads from cross-origin subresource
+// responses.
+BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE_PARAM(
+ bool,
+ kRestrictLinkHeaderOnSubresourceCrossOrigin);
+// Disables "rel=dns-prefetch" and "rel=preconnect".
+BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE_PARAM(
+ bool,
+ kRestrictLinkHeaderOnSubresourceNetworkHint);
+// Disables "rel=preload", "rel=modulepreload", and "rel=prefetch".
+BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE_PARAM(
+ bool,
+ kRestrictLinkHeaderOnSubresourceResourceLoad);
+
// When enabled, it adds Payto URI Scheme to the safe list for
// registerProtocolHandler. This feature is disabled by default
// Payto URI Scheme explanation https://datatracker.ietf.org/doc/html/rfc8905
diff --git a/third_party/blink/renderer/core/loader/preload_helper.cc b/third_party/blink/renderer/core/loader/preload_helper.cc
index cfc2dabe..75637de8 100644
--- a/third_party/blink/renderer/core/loader/preload_helper.cc
+++ b/third_party/blink/renderer/core/loader/preload_helper.cc
@@ -191,7 +191,12 @@
as == "video" || as == "worker" || as == "xslt";
}
-bool IsNetworkHintAllowed(PreloadHelper::LoadLinksFromHeaderMode mode) {
+bool IsNetworkHintAllowed(PreloadHelper::LoadLinksFromHeaderMode mode,
+ bool is_header_on_subresource) {
+ if (is_header_on_subresource &&
+ blink::features::kRestrictLinkHeaderOnSubresourceNetworkHint.Get()) {
+ return false;
+ }
switch (mode) {
case PreloadHelper::LoadLinksFromHeaderMode::kDocumentBeforeCommit:
return true;
@@ -211,7 +216,12 @@
}
bool IsResourceLoadAllowed(PreloadHelper::LoadLinksFromHeaderMode mode,
- bool is_viewport_dependent) {
+ bool is_viewport_dependent,
+ bool is_header_on_subresource) {
+ if (is_header_on_subresource &&
+ blink::features::kRestrictLinkHeaderOnSubresourceResourceLoad.Get()) {
+ return false;
+ }
switch (mode) {
case PreloadHelper::LoadLinksFromHeaderMode::kDocumentBeforeCommit:
return false;
@@ -231,7 +241,13 @@
}
bool IsCompressionDictionaryLoadAllowed(
- PreloadHelper::LoadLinksFromHeaderMode mode) {
+ PreloadHelper::LoadLinksFromHeaderMode mode,
+ bool is_header_on_subresource) {
+ if (is_header_on_subresource &&
+ blink::features::kRestrictLinkHeaderOnSubresourceCompressionDictionary
+ .Get()) {
+ return false;
+ }
// Document header can trigger dictionary load after the page load completes.
// Subresources header can trigger dictionary load if it is not from the
// memory cache.
@@ -828,11 +844,12 @@
if (!header.Valid() || header.Url().empty() || header.Rel().empty()) {
continue;
}
- bool is_network_hint_allowed = IsNetworkHintAllowed(mode);
- bool is_resource_load_allowed =
- IsResourceLoadAllowed(mode, header.IsViewportDependent());
+ bool is_network_hint_allowed =
+ IsNetworkHintAllowed(mode, is_subresource_load);
+ bool is_resource_load_allowed = IsResourceLoadAllowed(
+ mode, header.IsViewportDependent(), is_subresource_load);
bool is_compression_dictionary_load_allowed =
- IsCompressionDictionaryLoadAllowed(mode);
+ IsCompressionDictionaryLoadAllowed(mode, is_subresource_load);
if (!is_network_hint_allowed && !is_resource_load_allowed &&
!is_compression_dictionary_load_allowed) {
// Skip this `header`; it won't initiate any types of preloading.
@@ -856,6 +873,10 @@
.SetOriginStatusOnSubresource(base::to_underlying(origin_status))
.Record(document->UkmRecorder());
}
+ if (is_subresource_load && !from_same_origin &&
+ blink::features::kRestrictLinkHeaderOnSubresourceCrossOrigin.Get()) {
+ continue;
+ }
// For security purposes, set `referrerpolicy: "no-referrer"` in link loads
// from subresources. See https://crbug.com/415810136 for details.
Original Bug Report
Cross-origin image loaded in <img> tag can make additional request and leak referrer
VULNERABILITY DETAILS
See this tweet.
Chrome allows triggering additional network requests from subresources (e.g. images) by following Link header. AFAIK, resources loaded in <img> tag is inert, and therefore it can’t make additional network requests, animate (in case of SVG), etc.
This allows leaking referrer, which should be origin when cross-origin by default, which seems to be bypassed here.
CREDIT INFORMATION
Reporter credit: https://x.com/slonser_