CVE-2026-7966
Overview
Background
- LCP Critical Path Predictor (LCPP)
- a Chrome performance subsystem that predicts the Largest Contentful Paint element; its renderer side talks to the browser via the
blink::mojom::LCPCriticalPathPredictorHostMojo interface. - Interface binding (`Create`)
- the browser-side factory that, given a
RenderFrameHostand aPendingReceiver, instantiates and binds the host object to that frame. - `GetParentOrOuterDocument`
- a
RenderFrameHostaccessor returning the parent frame or outer document; it is non-null for subframes and for frames embedded in an outer document (e.g. fenced frames), and null only for the outermost main frame. - Outermost main frame
- the top-level document of a tab, the only frame for which
GetParentOrOuterDocument()is null.
Root Cause Analysis
The LCPP host-binding factory (Create in lcp_critical_path_predictor_host.cc) unconditionally constructed a LCPCriticalPathPredictorHost for whatever render_frame_host requested the interface, with no check that the frame was the top-level document. Because any frame — subframes, and embedded documents such as fenced frames — could bind the interface, a non-main frame (potentially cross-origin) could obtain an LCPP host it was not supposed to have, which is an insufficient-validation / site-isolation issue. The invariant that only the outermost main frame may use LCPP was not enforced at the binding boundary.
The fix adds an early frame gate at the top of Create: if render_frame_host->GetParentOrOuterDocument() is non-null (i.e. the frame is not the outermost main frame), the function returns before constructing the object, leaving the receiver unbound. Placing the guard before the new LCPCriticalPathPredictorHost(...) ensures the object is never created for disallowed frames.
Create on GetParentOrOuterDocument() being null so only the outermost main frame can bind it.Attack Path
- Run script in a non-main frame Load content in a subframe or an embedded document such as a fenced frame (potentially cross-origin to the top-level page).
- Request the LCPP interface
From that frame, trigger binding of
blink::mojom::LCPCriticalPathPredictorHost, reaching the browser-sideCreatefactory. - Obtain an unauthorized host (pre-fix)
Because
Createdid no frame-type check, it constructed and bound anLCPCriticalPathPredictorHostfor the non-main frame, giving it access to an interface reserved for the outermost main frame (inferred: cross-frame influence over LCPP state/hints).
Impact Assessment
Files Changed
chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_host.cc
Audit Directions
- Unguarded Mojo factory bindingAudit
Create/BindReceiverfactories for interfaces intended for the main frame to ensure they checkGetParentOrOuterDocument()/IsOutermostMainFrame()before constructing the host. - Fenced-frame / embedded-document reachFor any per-document interface, verify that outer-document embeddings (fenced frames, portals) cannot bind interfaces meant only for a top-level document.
- DocumentService without frame-type checksReview
DocumentService-derived hosts whose lifetime is tied to aRenderFrameHostbut which assume main-frame-only usage without enforcing it at bind time.
Patch
From d0efcafb889fec8a81c2fbc8abf305896c82a254 Mon Sep 17 00:00:00 2001 From: Minoru Chikamune <[email protected]> Date: Thu, 02 Apr 2026 00:52:40 -0700 Subject: [PATCH] [LCPP] Restrict LCPCriticalPathPredictorHost to the outermost main frame This CL adds a frame gate to LCPCriticalPathPredictorHost::Create to ensure that only the outermost main frame can bind the interface. Bug: 497341787 Change-Id: I1fd830f0b302810fb3afd00e61e25e4d9602084d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7724500 Reviewed-by: Yoshisato Yanagisawa <[email protected]> Commit-Queue: Minoru Chikamune <[email protected]> Cr-Commit-Position: refs/heads/main@{#1609011} --- diff --git a/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_host.cc b/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_host.cc index e204d3ea..d2edc6b 100644 --- a/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_host.cc +++ b/chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_host.cc @@ -30,6 +30,11 @@ content::RenderFrameHost* render_frame_host, mojo::PendingReceiver<blink::mojom::LCPCriticalPathPredictorHost> receiver) { + // Only valid for the main frame. + if (render_frame_host->GetParentOrOuterDocument()) { + return; + } + // The object is bound to the lifetime of the |render_frame_host| and the mojo // connection. See DocumentService for details. new LCPCriticalPathPredictorHost(*render_frame_host, std::move(receiver));
Original Bug Report
Site Isolation bypass via missing frame gate in LCPCriticalPathPredictorHost
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A compromised renderer in a cross-origin subframe can bind the LCPCriticalPathPredictorHost Mojo interface to inject arbitrary URLs into the parent frame’s prefetch database. When the user later navigates to the parent site, the browser automatically prefetches these injected URLs with full credentials, including SameSite=Strict cookies. This results in a persistent, credentialed blind GET CSRF against the embedder’s origin.
Affected files:
chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_host.ccchrome/browser/page_load_metrics/observers/lcp_critical_path_predictor_page_load_metrics_observer.ccchrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util.cc
Estimated timestamp from git blame: 2024-10-21
Summary
There is a potential Site Isolation bypass in the LCP Critical Path Predictor (LCPP) implementation. A compromised renderer hosting a cross-origin iframe can poison the LCPP prefetch database for the parent frame’s origin. By binding the LCPCriticalPathPredictorHost Mojo interface, which lacks a subframe check, an attacker can inject arbitrary same-site URLs. These URLs are then automatically prefetched with credentials (including SameSite=Strict cookies) when the user later navigates to the victim’s site.
Note: The following analysis is based on code review by an AI tooling agent. The steps below are a suggested/potential attack path, as we do not currently have the capability to run a live Proof of Concept.
Vulnerability Details
This issue arises from a combination of missing frame gating and incorrect page resolution during Mojo IPC handling:
- Missing Frame Gate:
LCPCriticalPathPredictorHost::Create()(chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_host.cc) fails to verify that the bindingRenderFrameHostis an outermost main frame. This contrasts with other predictors likeNavigationPredictor::Create(), which safely reject subframes usingGetParentOrOuterDocument(). - Page Resolution Escalation: When the compromised subframe renderer calls
NotifyFetchedSubresource, the browser retrieves thePageobject viarender_frame_host().GetPage(). Because aPagerepresents the entire document tree, calling this on a subframe returns the outermost main frame’sPage. - Unvalidated Data Append: The host then retrieves the
LcpCriticalPathPredictorPageLoadMetricsObserverattached to the main frame and callsAppendFetchedSubresourceUrl. This blindly records the attacker’s URL into the main frame’slcpp_data_inputs_without verifying the origin of the reporting frame. - Database Persistence: When the user navigates away or closes the tab,
FinalizeLCPpersists this data into the SQLite database, keying it to the main frame’s host. - Credentialed Execution: Upon a future navigation to the victim site (e.g., via the Omnibox, where
initiator_originis null), the browser reads the poisoned LCPP entry. It constructs a prefetch request (network::ResourceRequest) withsite_for_cookiesandrequest_initiatorset to the victim’s origin, resulting in a fully authenticated GET request that bypassesSameSiteand Fetch Metadata protections.
Suggested Attack Steps
- An attacker sets up a malicious page that is loaded as a cross-origin iframe on a benign site (
victim.com). - The attacker exploits a standard renderer bug (e.g., in V8) to gain code execution within their cross-origin subframe.
- The compromised renderer requests to bind
blink::mojom::LCPCriticalPathPredictorHost. - The attacker sends a
NotifyFetchedSubresourceIPC message containing a sensitive GET endpoint on the parent origin (e.g.,https://victim.com/sensitive_action?update=1). - The user eventually closes the tab or navigates away, saving the poisoned prediction to the browser’s local LCPP database under the
victim.comkey. - Days later, the user types
victim.cominto the Omnibox. - The browser attempts to optimize the load, retrieves the poisoned LCPP data, and issues a prefetch request to
https://victim.com/sensitive_action?update=1. Since the browser believes it is optimizing a first-party load, it attaches all first-party cookies (includingSameSite=Strict), resulting in a successful GET CSRF.
Suggested Fix
Add a subframe check in LCPCriticalPathPredictorHost::Create() to prevent cross-origin iframes from binding the interface:
void LCPCriticalPathPredictorHost::Create(
content::RenderFrameHost* render_frame_host,
mojo::PendingReceiver<blink::mojom::LCPCriticalPathPredictorHost> receiver) {
// Only valid for the main frame.
if (render_frame_host->GetParentOrOuterDocument()) {
return;
}
// ... existing code ...
}
Additionally, AppendFetchedSubresourceUrl should optionally validate that the reported subresource’s origin makes sense in the context of the reporting frame, though blocking the interface entirely for subframes is the most robust fix.
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.