Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Preload
DescriptionInappropriate implementation in Preload
ComponentPreload
Bug ClassLogic Error
Tracker497490364
Fix commitef0bfaeae0da (chromium/src) +14/-8
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
if
chrome/browser/predictors/loading_predictor_tab_helper.cc
modified
if
content/web_test/browser/web_test_control_host.cc
modified

Files Changed

  • chrome/browser/page_load_metrics/observers/lcp_critical_path_predictor_page_load_metrics_observer_unittest.cc
  • chrome/browser/predictors/loading_predictor_tab_helper.cc
  • content/browser/renderer_host/navigation_request.cc
  • content/browser/renderer_host/navigation_request.h
  • content/public/browser/navigation_handle.h
  • content/public/test/mock_navigation_handle.h
  • content/web_test/browser/web_test_control_host.cc
From ef0bfaeae0da852f8bb117cfa323af929b904b87 Mon Sep 17 00:00:00 2001
From: Minoru Chikamune <[email protected]>
Date: Thu, 02 Apr 2026 22:41:46 -0700
Subject: [PATCH] Clear LCPP hint during redirects to prevent cross-origin info leak

During redirects, stale LCPP (LCP Critical Path Predictor) hints from
the initial origin could leak to the redirect target's renderer process.
This could potentially be used for XS-Search attacks.

This CL ensures that the LCPP hint is properly cleared during redirects.

Bug: 497490364
Bug: 40063266
Change-Id: I196a8e26f6403f6eb9571aa1d19411f182600af4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7725921
Reviewed-by: Lingqi Chi <[email protected]>
Reviewed-by: Takashi Toyoshima <[email protected]>
Reviewed-by: Yoshisato Yanagisawa <[email protected]>
Reviewed-by: Rakina Zata Amni <[email protected]>
Commit-Queue: Minoru Chikamune <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1609673}
---

diff --git a/chrome/browser/page_load_metrics/observers/lcp_critical_path_predictor_page_load_metrics_observer_unittest.cc b/chrome/browser/page_load_metrics/observers/lcp_critical_path_predictor_page_load_metrics_observer_unittest.cc
index 7eb5e14..7ea1485 100644
--- a/chrome/browser/page_load_metrics/observers/lcp_critical_path_predictor_page_load_metrics_observer_unittest.cc
+++ b/chrome/browser/page_load_metrics/observers/lcp_critical_path_predictor_page_load_metrics_observer_unittest.cc
@@ -107,7 +107,7 @@
   void ProvideLCPPHint(content::NavigationSimulator* navigation) {
     blink::mojom::LCPCriticalPathPredictorNavigationTimeHint hint;
     hint.lcp_element_locators = {"foo"};
-    navigation->GetNavigationHandle()->SetLCPPNavigationHint(hint);
+    navigation->GetNavigationHandle()->SetLCPPNavigationHint(hint.Clone());
   }
 
   void SetMockLcpElementLocator(
diff --git a/chrome/browser/predictors/loading_predictor_tab_helper.cc b/chrome/browser/predictors/loading_predictor_tab_helper.cc
index 2d0f776..8542a27a 100644
--- a/chrome/browser/predictors/loading_predictor_tab_helper.cc
+++ b/chrome/browser/predictors/loading_predictor_tab_helper.cc
@@ -200,6 +200,7 @@
 void MaybeSetLCPPNavigationHint(content::NavigationHandle& navigation_handle,
                                 LoadingPredictor& predictor) {
   TRACE_EVENT("navigation", "MaybeSetLCPPNavigationHint");
+  navigation_handle.SetLCPPNavigationHint(nullptr);
   base::ElapsedTimer timer;
   if (!blink::LcppEnabled() || !navigation_handle.IsInOutermostMainFrame() ||
       navigation_handle.IsSameDocument()) {
@@ -221,7 +222,7 @@
     hint->for_testing = true;
   }
   if (hint) {
-    navigation_handle.SetLCPPNavigationHint(*hint);
+    navigation_handle.SetLCPPNavigationHint(hint->Clone());
     base::UmaHistogramEnumeration(
         "LoadingPredictor.SetLCPPNavigationHint.Status",
         LcppHintStatus::kSucceedToSet);
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index 4cc50c0..03a1646 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -3643,6 +3643,10 @@
   // for the redirected one.
   commit_params_->not_restored_reasons = nullptr;
 
+  // Reset the LCPP hint as the hint is for the original page and not for the
+  // redirected one.
+  commit_params_->lcpp_hint = nullptr;
+
   // Reset the tentative origin_to_commit, as the redirected one is different.
   tentative_data_origin_to_commit_ = std::nullopt;
 
@@ -9526,10 +9530,10 @@
 }
 
 void NavigationRequest::SetLCPPNavigationHint(
-    const blink::mojom::LCPCriticalPathPredictorNavigationTimeHint& hint) {
+    blink::mojom::LCPCriticalPathPredictorNavigationTimeHintPtr hint) {
   CHECK(WILL_START_REQUEST == state_ || WILL_REDIRECT_REQUEST == state_)
       << state_;
-  commit_params_->lcpp_hint = hint.Clone();
+  commit_params_->lcpp_hint = std::move(hint);
 }
 
 const blink::mojom::LCPCriticalPathPredictorNavigationTimeHintPtr&
diff --git a/content/browser/renderer_host/navigation_request.h b/content/browser/renderer_host/navigation_request.h
index 5aebd5e7..1deeb9613 100644
--- a/content/browser/renderer_host/navigation_request.h
+++ b/content/browser/renderer_host/navigation_request.h
@@ -427,7 +427,7 @@
   void SetRequestHeader(std::string_view header_name,
                         std::string_view header_value) override;
   void SetLCPPNavigationHint(
-      const blink::mojom::LCPCriticalPathPredictorNavigationTimeHint& hint)
+      blink::mojom::LCPCriticalPathPredictorNavigationTimeHintPtr hint)
       override;
   const blink::mojom::LCPCriticalPathPredictorNavigationTimeHintPtr&
   GetLCPPNavigationHint() override;
diff --git a/content/public/browser/navigation_handle.h b/content/public/browser/navigation_handle.h
index b216fe9..aefbc369 100644
--- a/content/public/browser/navigation_handle.h
+++ b/content/public/browser/navigation_handle.h
@@ -527,7 +527,7 @@
   // Set LCP Critical Path Predictor hint data to be passed along to the
   // renderer process on the navigation commit.
   virtual void SetLCPPNavigationHint(
-      const blink::mojom::LCPCriticalPathPredictorNavigationTimeHint& hint) = 0;
+      blink::mojom::LCPCriticalPathPredictorNavigationTimeHintPtr hint) = 0;
 
   // Peek into LCP Critical Path Predictor hint data attached to the navigation.
   virtual const blink::mojom::LCPCriticalPathPredictorNavigationTimeHintPtr&
diff --git a/content/public/test/mock_navigation_handle.h b/content/public/test/mock_navigation_handle.h
index 5c3b092..e0778a98 100644
--- a/content/public/test/mock_navigation_handle.h
+++ b/content/public/test/mock_navigation_handle.h
@@ -30,6 +30,7 @@
 #include "net/http/http_response_headers.h"
 #include "services/metrics/public/cpp/ukm_source_id.h"
 #include "testing/gmock/include/gmock/gmock.h"
+#include "third_party/blink/public/mojom/lcp_critical_path_predictor/lcp_critical_path_predictor.mojom.h"
 #include "third_party/blink/public/mojom/loader/referrer.mojom.h"
 #include "third_party/blink/public/mojom/loader/transferrable_url_loader.mojom.h"
 #include "third_party/blink/public/mojom/navigation/renderer_content_settings.mojom.h"
@@ -181,7 +182,7 @@
   }
   MOCK_METHOD1(
       SetLCPPNavigationHint,
-      void(const blink::mojom::LCPCriticalPathPredictorNavigationTimeHint&));
+      void(blink::mojom::LCPCriticalPathPredictorNavigationTimeHintPtr));
   MOCK_METHOD0(
       GetLCPPNavigationHint,
       const blink::mojom::LCPCriticalPathPredictorNavigationTimeHintPtr&());
diff --git a/content/web_test/browser/web_test_control_host.cc b/content/web_test/browser/web_test_control_host.cc
index 05827c4..6e59d62 100644
--- a/content/web_test/browser/web_test_control_host.cc
+++ b/content/web_test/browser/web_test_control_host.cc
@@ -1133,7 +1133,7 @@
 void WebTestControlHost::DidStartNavigation(
     NavigationHandle* navigation_handle) {
   if (lcpp_hint_) {
-    navigation_handle->SetLCPPNavigationHint(lcpp_hint_.value());
+    navigation_handle->SetLCPPNavigationHint(lcpp_hint_->Clone());
   }
 }
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/page_load_metrics/observers/lcp_critical_path_predictor_page_load_metrics_observer_unittest.cc b/chrome/browser/page_load_metrics/observers/lcp_critical_path_predictor_page_load_metrics_observer_unittest.cc
index 7eb5e14..7ea1485 100644
--- a/chrome/browser/page_load_metrics/observers/lcp_critical_path_predictor_page_load_metrics_observer_unittest.cc
+++ b/chrome/browser/page_load_metrics/observers/lcp_critical_path_predictor_page_load_metrics_observer_unittest.cc
@@ -107,7 +107,7 @@
   void ProvideLCPPHint(content::NavigationSimulator* navigation) {
     blink::mojom::LCPCriticalPathPredictorNavigationTimeHint hint;
     hint.lcp_element_locators = {"foo"};
-    navigation->GetNavigationHandle()->SetLCPPNavigationHint(hint);
+    navigation->GetNavigationHandle()->SetLCPPNavigationHint(hint.Clone());
   }
 
   void SetMockLcpElementLocator(
diff --git a/content/public/test/mock_navigation_handle.h b/content/public/test/mock_navigation_handle.h
index 5c3b092..e0778a98 100644
--- a/content/public/test/mock_navigation_handle.h
+++ b/content/public/test/mock_navigation_handle.h
@@ -30,6 +30,7 @@
 #include "net/http/http_response_headers.h"
 #include "services/metrics/public/cpp/ukm_source_id.h"
 #include "testing/gmock/include/gmock/gmock.h"
+#include "third_party/blink/public/mojom/lcp_critical_path_predictor/lcp_critical_path_predictor.mojom.h"
 #include "third_party/blink/public/mojom/loader/referrer.mojom.h"
 #include "third_party/blink/public/mojom/loader/transferrable_url_loader.mojom.h"
 #include "third_party/blink/public/mojom/navigation/renderer_content_settings.mojom.h"
@@ -181,7 +182,7 @@
   }
   MOCK_METHOD1(
       SetLCPPNavigationHint,
-      void(const blink::mojom::LCPCriticalPathPredictorNavigationTimeHint&));
+      void(blink::mojom::LCPCriticalPathPredictorNavigationTimeHintPtr));
   MOCK_METHOD0(
       GetLCPPNavigationHint,
       const blink::mojom::LCPCriticalPathPredictorNavigationTimeHintPtr&());
Loading diff…

Original Bug Report

reported by [email protected]

Cross-origin info leak via stale LCPP hint during redirects

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: During cross-origin redirects, the browser fails to clear the LCPP (LCP Critical Path Predictor) hint if the redirect target has no predictor data. This allows stale navigation hints, including LCP element locators, from the initial origin to leak to the redirect target’s renderer, potentially enabling XS-Search attacks.

Affected files:

  • chrome/browser/predictors/loading_predictor_tab_helper.cc
  • content/browser/renderer_host/navigation_request.cc
  • content/renderer/render_frame_impl.cc
  • third_party/blink/renderer/core/frame/web_local_frame_impl.cc

Estimated timestamp from git blame: 2024-01-09

Summary

There is a potential cross-origin information leak in Chrome’s LCP Critical Path Predictor (LCPP) feature. When a navigation is redirected to a new origin, the browser process may fail to clear the LCPP hint associated with the initial origin. This stale hint is then committed to the new origin’s renderer. An attacker can exploit this to infer a user’s DOM structure on a victim site by observing which lazy-loaded images are eagerly preloaded.

Technical Details

The LCPP feature optimizes page loads by predicting subresources like LCP elements based on historical data. This data is attached to a navigation via NavigationRequest::commit_params_->lcpp_hint.

Root Cause 1: Missing reset in MaybeSetLCPPNavigationHint

In chrome/browser/predictors/loading_predictor_tab_helper.cc, MaybeSetLCPPNavigationHint populates the hint. It is called during DidStartNavigation and DidRedirectNavigation. If a redirect leads to a URL for which the predictor has no data, GetLCPPHint returns std::nullopt. In this case, MaybeSetLCPPNavigationHint returns early without explicitly clearing any existing hint from the navigation_handle.

Root Cause 2: Missing reset in NavigationRequest

In content/browser/renderer_host/navigation_request.cc, OnRequestRedirected resets parameters that should not persist across redirects (e.g., page_state). However, it fails to clear lcpp_hint.

Propagation and Exploitation

Upon commit, content/renderer/render_frame_impl.cc forwards the stale hint to the renderer via frame_->SetLCPPHint().

With the kLCPPLazyLoadImagePreload feature enabled by default, the HTMLPreloadScanner uses the LCPP element locators to eagerly preload images matching the locators, even if they have loading="lazy". An attacker can use this as an XS-Search oracle.

Potential Exploitation Steps

(Note: These are suggested steps; we do not yet have a working proof of concept that has been successfully run.)

  1. An attacker identifies an open redirect on a victim site (e.g., https://victim.example/?redirect=https://attacker.example).
  2. A user who frequently visits the victim site (accumulating LCPP data) clicks the open redirect link.
  3. During navigation start, DidStartNavigation attaches the victim’s LCPP hint to the NavigationRequest.
  4. During the redirect, DidRedirectNavigation finds no LCPP data for attacker.example. It fails to clear the existing hint.
  5. The navigation commits, and the attacker’s renderer receives the victim’s LCPP hint.
  6. The attacker’s page contains multiple hidden <img loading="lazy"> tags wrapped in various DOM structures (e.g., <div id="profile"><img class="avatar" loading="lazy">).
  7. The browser eagerly preloads the specific image that matches the victim’s leaked LCP element locator, while deferring the others. The attacker observes the fetch on their server, revealing the victim’s DOM state.

Suggested Fix

  1. In content/browser/renderer_host/navigation_request.cc, update NavigationRequest::OnRequestRedirected to clear commit_params_->lcpp_hint alongside other cross-origin sensitive state.
  2. In chrome/browser/predictors/loading_predictor_tab_helper.cc, update MaybeSetLCPPNavigationHint to explicitly clear the hint on the navigation_handle if GetLCPPHint returns std::nullopt.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker