High patch-now CVSS 4.3 chrome Policy Bypass ⚠️ Exploited in the wild 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
4.3
CVSS
Yes
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Loader. Source: X post from @slonser_
DescriptionInsufficient policy enforcement in Loader. Source: X post from @slonser_
ComponentChromium
Sub-componentBlink>Loader
Bug ClassPolicy Bypass
Action tierpatch-now — exploited in the wild / KEV (itw) — real-world evidence overrides analysis
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker415810136
Fix commit1ea5b9417cd9 (chromium/src) +72/-7
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedMicky
Reported2025-04-22 (per vendor advisory)
Fix landed2025-12-10 (public git)
Disclosed2025-05-14

Background

HTTP `Link` header
a response header that can declare relationships such as rel=preload, rel=modulepreload, rel=prefetch, rel=dns-prefetch, rel=preconnect, and rel=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 Link header can chain additional preloads.
`PreloadHelper` link processing
Blink’s preload_helper.cc logic (third_party/blink/renderer/core/loader/preload_helper.cc) that walks Link headers 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-category BASE_FEATURE_PARAM sub-flags used to selectively disable Link-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.

Key insight
The core mistake was treating a subresource response’s 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

  1. Serve a crafted subresource An attacker controls a subresource fetched by a victim page (for example a cross-origin resource) and attaches an HTTP Link header declaring a preload/prefetch/network-hint/compression-dictionary relationship.
  2. Header is processed for the subresource The preload_helper.cc loop runs the Link header with is_subresource_load true, and pre-patch IsNetworkHintAllowed / IsResourceLoadAllowed / IsCompressionDictionaryLoadAllowed allowed it without any subresource- or cross-origin-specific restriction.
  3. Chained load is initiated The subresource-supplied Link header causes the browser to initiate a further network request (network hint, resource load, or dictionary load) from the subresource response context.
  4. 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

An attacker able to control a subresource response could use its 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

FunctionChangeNotes
switch
third_party/blink/renderer/core/loader/preload_helper.cc
modified

Files Changed

  • third_party/blink/common/features.cc
  • third_party/blink/public/common/features.h
  • third_party/blink/renderer/core/loader/preload_helper.cc

Audit Directions

  • Header-driven side effects from subresources
    Audit 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 predicates
    Look 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 surfaces
    Review preload, modulepreload, prefetch, dns-prefetch, preconnect, and compression-dictionary initiation paths for whether cross-origin subresources can initiate them and whether referrer/policy defaults are applied.
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.
Loading diff…

Original Bug Report

reported by se...@gmail.com

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_

View on issue tracker