Low watch CVSS 4.3 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Preload
DescriptionInappropriate implementation in Preload
ComponentPreload
Bug ClassLogic Error
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker497490364
Fix commitef0bfaeae0da (chromium/src) +14/-8
CWECWE-693
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-29 (per vendor advisory)
Fix landed2026-04-03 (public git)
Disclosed2026-05-05
Patch gap32 days — fix public in source before the advisory

Background

LCPP (LCP Critical Path Predictor)
a Preload optimization that predicts the Largest Contentful Paint element for a page and ships a per-navigation hint (blink::mojom::LCPCriticalPathPredictorNavigationTimeHint) to the renderer at commit time.
`NavigationRequest`
the browser-side object (content/browser/renderer_host/navigation_request.cc) that tracks a single navigation, including any redirects, and carries commit_params_ to the renderer.
`commit_params_->lcpp_hint`
the field that transports the LCPP hint to whichever renderer process the navigation ultimately commits in.
XS-Search
a cross-site attack that infers information about another origin’s content from observable side effects such as timing or resource behavior.

Root Cause Analysis

When a navigation to origin A was computed, the browser attached an LCPP hint (derived from A’s predicted critical path) to commit_params_->lcpp_hint. If the server then issued a redirect to a different origin B, the redirect-handling path in NavigationRequest reset several per-origin fields (for example commit_params_->not_restored_reasons) but did not clear lcpp_hint. The stale hint computed for origin A therefore survived into the commit for origin B and was delivered to B’s renderer, violating the invariant that navigation-time hints must reflect the origin actually being committed.

The fix adds commit_params_->lcpp_hint = nullptr; on the redirect path, and reworks SetLCPPNavigationHint to take ownership of a ...Ptr (so nullptr can be passed to explicitly clear the hint), with MaybeSetLCPPNavigationHint now unconditionally clearing the hint at entry before possibly re-setting it.

Key insight
The redirect path reset per-origin navigation state but forgot the LCPP hint, so a hint built for the original origin leaked to the redirect target’s renderer; the fix nulls lcpp_hint on redirect and makes the setter accept ownership so clearing is a first-class operation.

Attack Path

  1. Victim starts a navigation The browser computes an LCPP hint for the initially requested origin A and stores it in commit_params_->lcpp_hint.
  2. Server redirects cross-origin A responds with a redirect to attacker-influenced origin B; the redirect handler resets some fields but (pre-patch) leaves lcpp_hint populated with A’s data.
  3. Stale hint commits to B The navigation commits in B’s renderer with A’s LCPP hint still attached, exposing predicted-element information derived from origin A.
  4. Information inference An attacker observing B’s renderer behavior could use the leaked hint as an XS-Search signal to infer details about origin A’s content (inferred; the patch demonstrates the leak, not a specific extraction primitive).

Impact Assessment

The advisory rates this low severity (Inappropriate implementation in Preload). An attacker gains a potential cross-origin information-leak channel: LCPP hint data for the pre-redirect origin is exposed to the post-redirect origin’s renderer, usable as an XS-Search side channel (inferred). The precondition is a navigation that redirects across origins; no memory corruption or code execution is shown by the patch.

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

Audit Directions

  • Per-origin state on redirect
    Audit every field of commit_params_/common_params_ reset (or not reset) in the NavigationRequest redirect path; any origin-derived hint, header, or predictor left unchanged across a cross-origin redirect is a candidate leak.
  • Const-ref setters that cannot clear
    Look for setters taking const T& that always store a clone; they make “no value” hard to express, so audit whether a stale value can persist when the intended state is “none”.
  • Speculative/preload hints crossing security boundaries
    Review other Preload/LoadingPredictor hints (prefetch, prerender, resource hints) for the same pattern of computing on origin A and committing on origin B after redirect.
From ef0bfaeae0da852f8bb117cfa323af929b904b87 Mon Sep 17 00:00:00 2001
From: Minoru Chikamune <chikamune@chromium.org>
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 <lingqi@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Reviewed-by: Yoshisato Yanagisawa <yyanagisawa@chromium.org>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Commit-Queue: Minoru Chikamune <chikamune@chromium.org>
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 vm...@google.com

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