CVE-2026-79059
Overview
Background
- `BackForwardCacheMetrics`
- A ref-counted browser-side object associated with a main-frame document and shared between all
NavigationEntryImpls with the same main-documentdocument_sequence_number_; among other things it carries theNotRestoredReasonstree for that document. - Document sequence number (DSN)
- An identifier for a document within a history entry; in this code path the committing value arrives as
params.document_sequence_number, i.e. it is supplied by the renderer in commit parameters. - `NotRestoredReasons` API
- A web-exposed API that reports to a page why its back/forward navigation was not served from BFCache, including a per-frame reasons tree, so its contents must belong to the document it is reported to.
- `SiteInstance`
- The browser’s grouping of documents by site for process isolation; a same-document main-frame navigation never changes the
SiteInstance.
Root Cause Analysis
BackForwardCacheMetrics::CreateOrReuseBackForwardCacheMetricsForNavigation(), called from the navigation-commit path in navigation_controller_impl.cc, decides whether a new navigation entry should share the previous entry’s metrics object. Before the patch, for a main-frame navigation it returned previous_entry_metrics whenever committing_document_sequence_number == previous_entry_metrics->document_sequence_number_, treating a DSN match alone as proof the navigation was same-document. Because that DSN comes from the renderer and, per the commit message, is not a reliable same-document signal, a main-frame commit in an unrelated cross-site SiteInstance could be bound to the previous entry’s metrics, including its NotRestoredReasons tree, which could later be reported through the NotRestoredReasons API to the wrong document.
The fix adds a SiteInstanceImpl* committing_main_frame_site_instance parameter (the caller passes rfh->GetSiteInstance() for main-frame navigations, nullptr otherwise) and tightens the main-frame reuse condition to also require previous_entry->site_instance() == committing_main_frame_site_instance. When the previous entry has no SiteInstance (e.g. after session restore) the old DSN-only behavior is deliberately retained, and subframe navigations still always reuse the metrics.
SiteInstance.Attack Path
- Victim document creates metrics
A main-frame document on one site commits and its
NavigationEntryImplgets aBackForwardCacheMetricsobject holding that document’s DSN and, later, itsNotRestoredReasonsdata. - Cross-site main-frame commit reports a matching DSN
A subsequent main-frame navigation commits in a different
SiteInstancebut its commit params carry adocument_sequence_numberequal to the previous entry’s (as modelled by the regression test); doing this deliberately most plausibly requires a compromised renderer or an unusual DSN collision (inferred). - Metrics object is reused
Pre-patch,
CreateOrReuseBackForwardCacheMetricsForNavigation()saw only the DSN match and returnedprevious_entry_metricsfor the new entry. - Reasons reported to the wrong document
The previous document’s
NotRestoredReasonstree is later surfaced via theNotRestoredReasonsAPI for the unrelated cross-site entry, disclosing information about the other document.
Impact Assessment
NotRestoredReasons data that belongs to a different site’s document, which may reveal details of that document’s frames or of why it could not be cached (exact fields exposed are inferred, as the diff does not show the reporting code). The leak is in data the browser process hands back to web content, but reaching the mismatched-DSN condition likely depends on a compromised renderer or a rare DSN collision (inferred), and the existing kCheckDocumentSequenceNumber block is noted in the code as not enforceable on Android. The advisory states only “Information leak in BFCache” and reports no in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
NavigationEntryImplcontent/browser/back_forward_cache/back_forward_cache_metrics.h |
modified | |
NavigationRequestcontent/browser/back_forward_cache/back_forward_cache_metrics.h |
modified | |
RenderFrameHostImplcontent/browser/back_forward_cache/back_forward_cache_metrics.h |
modified | |
SiteInstanceImplcontent/browser/back_forward_cache/back_forward_cache_metrics.h |
modified | |
BackForwardCacheMetricscontent/browser/back_forward_cache/back_forward_cache_metrics.h |
modified | |
CONTENT_EXPORTcontent/browser/back_forward_cache/back_forward_cache_metrics.h |
modified | |
TestObservercontent/browser/back_forward_cache/back_forward_cache_metrics.h |
modified | |
TEST_Fcontent/browser/back_forward_cache/back_forward_cache_metrics_unittest.cc |
modified |
Files Changed
content/browser/back_forward_cache/back_forward_cache_metrics_unittest.cccontent/browser/back_forward_cache/back_forward_cache_metrics.cccontent/browser/back_forward_cache/back_forward_cache_metrics.hcontent/browser/renderer_host/navigation_controller_impl.cc
Audit Directions
- Renderer-supplied identity in commit paramsAudit other uses of
document_sequence_number,item_sequence_numberand similarDidCommitProvisionalLoadParamsfields where a match is used to share or attach browser-side state across entries; confirm each is cross-checked against browser-known facts (SiteInstance, origin,RenderFrameHost). - Web-exposed diagnostics carrying cross-document dataReview APIs such as
NotRestoredReasonsand other navigation/performance timing reports for paths where state attached to aNavigationEntryImplcan be inherited by an entry of a different site. - Fallback branches that skip the new checkExamine the null-
SiteInstance(session restore) fallback and the Android path wherekCheckDocumentSequenceNumberis not enforced, to see whether a DSN-only decision can still be reached by a cross-site commit.
Patch
From 67e1aad4cc16ee14b6ed7d6a2057e36695ca8556 Mon Sep 17 00:00:00 2001 From: Anna Sato <[email protected]> Date: Thu, 23 Jul 2026 23:12:41 -0700 Subject: [PATCH] [BFCache] Gate metrics reuse on main-frame SiteInstance CreateOrReuseBackForwardCacheMetricsForNavigation reused the previous NavigationEntry's BackForwardCacheMetrics whenever the renderer-supplied document sequence number matched, regardless of SiteInstance. The document sequence number alone is not a reliable signal that two main-frame commits belong to the same document, so the previous entry's NotRestoredReasons tree could end up attached to an unrelated cross-site entry and later be reported via the NotRestoredReasons API for the wrong document. Same-document main-frame navigations never change SiteInstance, so require the committing main-frame SiteInstance to match the previous entry's before treating a document sequence number match as same-document. A null previous-entry SiteInstance (e.g. after session restore) continues to rely on the sequence number alone, matching the existing behaviour in SetSkippableForSameDocumentEntries. Also adds a unit test covering both the cross-SiteInstance and same-SiteInstance cases. TAG=agy CONV=23a358e0-79f8-4fd1-8ddc-2be156279091 Bug: 520121111 Fixed: 520121111 Change-Id: I67b37359f65dbbf156bad242ec9f2ba10435ba66 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8137640 Commit-Queue: Anna Sato <[email protected]> Reviewed-by: Rakina Zata Amni <[email protected]> Cr-Commit-Position: refs/heads/main@{#1667662} --- diff --git a/content/browser/back_forward_cache/back_forward_cache_metrics.cc b/content/browser/back_forward_cache/back_forward_cache_metrics.cc index 3283359..8c76e1bb 100644 --- a/content/browser/back_forward_cache/back_forward_cache_metrics.cc +++ b/content/browser/back_forward_cache/back_forward_cache_metrics.cc @@ -95,7 +95,8 @@ BackForwardCacheMetrics::CreateOrReuseBackForwardCacheMetricsForNavigation( NavigationEntryImpl* previous_entry, bool is_main_frame_navigation, - int64_t committing_document_sequence_number) { + int64_t committing_document_sequence_number, + SiteInstanceImpl* committing_main_frame_site_instance) { // TODO(https://crbug.com/445585641): Make this enforceable on Android. #if !BUILDFLAG(IS_ANDROID) if (base::FeatureList::IsEnabled(kCheckDocumentSequenceNumber)) { @@ -123,10 +124,18 @@ } // Reuse `previous_entry_metrics` on subframe navigations and same-document - // navigations. + // main-frame navigations. The document sequence number used to detect the + // latter is supplied by the renderer; since same-document navigations never + // change SiteInstance, also require the committing SiteInstance to match + // `previous_entry`'s before treating the navigation as same-document. + // `previous_entry`'s SiteInstance may be null after session restore, in + // which case the document sequence number alone is trusted. if (!is_main_frame_navigation || - committing_document_sequence_number == - previous_entry_metrics->document_sequence_number_) { + (committing_document_sequence_number == + previous_entry_metrics->document_sequence_number_ && + (!previous_entry->site_instance() || + previous_entry->site_instance() == + committing_main_frame_site_instance))) { return previous_entry_metrics; } diff --git a/content/browser/back_forward_cache/back_forward_cache_metrics.h b/content/browser/back_forward_cache/back_forward_cache_metrics.h index 00f9887..6c5706a 100644 --- a/content/browser/back_forward_cache/back_forward_cache_metrics.h +++ b/content/browser/back_forward_cache/back_forward_cache_metrics.h @@ -42,13 +42,14 @@ class NavigationEntryImpl; class NavigationRequest; class RenderFrameHostImpl; +class SiteInstanceImpl; struct BackForwardCacheCanStoreDocumentResultWithTree; // Helper class for recording metrics around history navigations. // Associated with a main frame document and shared between all // NavigationEntries with the same document_sequence_number for the main // document. -class BackForwardCacheMetrics +class CONTENT_EXPORT BackForwardCacheMetrics : public base::RefCounted<BackForwardCacheMetrics> { public: using NotRestoredReason = BackForwardCache::NotRestoredReason; @@ -99,13 +100,17 @@ // navigation is a subframe navigation or if it's same-document with // `previous_entry`'s document. // - // |document_sequence_number| is the sequence number of the document - // associated with the navigating frame. + // `committing_document_sequence_number` is the sequence number of the + // document associated with the navigating frame. + // `committing_main_frame_site_instance` is the SiteInstance the main frame + // is committing in, and is only used when `is_main_frame_navigation` is + // true. static scoped_refptr<BackForwardCacheMetrics> CreateOrReuseBackForwardCacheMetricsForNavigation( NavigationEntryImpl* previous_entry, bool is_main_frame_navigation, - int64_t committing_document_sequence_number); + int64_t committing_document_sequence_number, + SiteInstanceImpl* committing_main_frame_site_instance); explicit BackForwardCacheMetrics(int64_t document_sequence_number); @@ -188,11 +193,11 @@ // Exported for testing. // The DisabledReason's source and id combined to give a unique uint64. - CONTENT_EXPORT static uint64_t MetricValue(BackForwardCache::DisabledReason); + static uint64_t MetricValue(BackForwardCache::DisabledReason); // Injects a clock for mocking time. // Should be called only from the UI thread. - CONTENT_EXPORT static void OverrideTimeForTesting(base::TickClock* clock); + static void OverrideTimeForTesting(base::TickClock* clock); class TestObserver { public: diff --git a/content/browser/renderer_host/navigation_controller_impl.cc b/content/browser/renderer_host/navigation_controller_impl.cc index f6fd7bb..b02d351c 100644 --- a/content/browser/renderer_host/navigation_controller_impl.cc +++ b/content/browser/renderer_host/navigation_controller_impl.cc @@ -1916,7 +1916,8 @@ back_forward_cache_metrics = BackForwardCacheMetrics:: CreateOrReuseBackForwardCacheMetricsForNavigation( GetLastCommittedEntry(), is_main_frame_navigation, - params.document_sequence_number); + params.document_sequence_number, + is_main_frame_navigation ? rfh->GetSiteInstance() : nullptr); } // Notify the last active entry that we have navigated away. diff --git a/content/browser/back_forward_cache/back_forward_cache_metrics_unittest.cc b/content/browser/back_forward_cache/back_forward_cache_metrics_unittest.cc index 2d2cef5f..620d544 100644 --- a/content/browser/back_forward_cache/back_forward_cache_metrics_unittest.cc +++ b/content/browser/back_forward_cache/back_forward_cache_metrics_unittest.cc @@ -2,12 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "content/browser/back_forward_cache/back_forward_cache_metrics.h" + #include "base/sanitizer_buildflags.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/scoped_feature_list.h" #include "base/test/simple_test_tick_clock.h" #include "components/ukm/test_ukm_recorder.h" #include "content/browser/back_forward_cache/back_forward_cache_impl.h" +#include "content/browser/renderer_host/navigation_controller_impl.h" +#include "content/browser/renderer_host/navigation_entry_impl.h" +#include "content/browser/site_instance_impl.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/web_contents_delegate.h" #include "content/public/browser/web_contents_observer.h" @@ -388,6 +393,49 @@ BackForwardCacheMetrics::HistoryNavigationOutcome::kNotRestored, 1); } +TEST_F(BackForwardCacheMetricsTest, + MetricsNotReusedForCrossSiteInstanceMainFrameNavigation) { + const GURL url1("http://a.com/"); + const GURL url2("http://b.com/"); + + NavigationControllerImpl& controller = + static_cast<NavigationControllerImpl&>(contents()->GetController()); + + // Navigate to `url1` and capture the entry's metrics, SiteInstance and + // document sequence number. + NavigationSimulator::NavigateAndCommitFromDocument(url1, main_test_rfh()); + NavigationEntryImpl* entry1 = controller.GetEntryAtIndex(0); + BackForwardCacheMetrics* metrics1 = entry1->back_forward_cache_metrics(); + SiteInstanceImpl* site_instance1 = entry1->site_instance(); + int64_t dsn1 = entry1->root_node()->frame_entry->document_sequence_number(); + ASSERT_TRUE(metrics1); + ASSERT_TRUE(site_instance1); + + // Navigate cross-site to `url2`, which commits in a different SiteInstance. + NavigationSimulator::NavigateAndCommitFromDocument(url2, main_test_rfh()); + NavigationEntryImpl* entry2 = controller.GetEntryAtIndex(1); + SiteInstanceImpl* site_instance2 = entry2->site_instance(); + ASSERT_TRUE(site_instance2); + ASSERT_NE(site_instance1, site_instance2); + ASSERT_NE(entry2->back_forward_cache_metrics(), metrics1); + + // A main-frame navigation that reports `entry1`'s document sequence number + // but commits in `entry2`'s SiteInstance must not reuse `entry1`'s metrics, + // since the document sequence number is renderer-supplied and a + // same-document navigation never changes SiteInstance. + scoped_refptr<BackForwardCacheMetrics> result = BackForwardCacheMetrics:: + CreateOrReuseBackForwardCacheMetricsForNavigation( + entry1, /*is_main_frame_navigation=*/true, dsn1, site_instance2); + EXPECT_NE(result.get(), metrics1); + + // A main-frame navigation reporting `entry1`'s document sequence number and + // committing in `entry1`'s SiteInstance should still reuse the metrics. + result = BackForwardCacheMetrics:: + CreateOrReuseBackForwardCacheMetricsForNavigation( + entry1, /*is_main_frame_navigation=*/true, dsn1, site_instance1); + EXPECT_EQ(result.get(), metrics1); +} + TEST_F(BackForwardCacheMetricsTest, PageWithFormsMetricsNotStore) { DisableBackForwardCacheForTesting(contents(), BackForwardCache::TEST_REQUIRES_NO_CACHING);
Regression Test / PoC
diff --git a/content/browser/back_forward_cache/back_forward_cache_metrics_unittest.cc b/content/browser/back_forward_cache/back_forward_cache_metrics_unittest.cc
index 2d2cef5f..620d544 100644
--- a/content/browser/back_forward_cache/back_forward_cache_metrics_unittest.cc
+++ b/content/browser/back_forward_cache/back_forward_cache_metrics_unittest.cc
@@ -2,12 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include "content/browser/back_forward_cache/back_forward_cache_metrics.h"
+
#include "base/sanitizer_buildflags.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_tick_clock.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/back_forward_cache/back_forward_cache_impl.h"
+#include "content/browser/renderer_host/navigation_controller_impl.h"
+#include "content/browser/renderer_host/navigation_entry_impl.h"
+#include "content/browser/site_instance_impl.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/browser/web_contents_observer.h"
@@ -388,6 +393,49 @@
BackForwardCacheMetrics::HistoryNavigationOutcome::kNotRestored, 1);
}
+TEST_F(BackForwardCacheMetricsTest,
+ MetricsNotReusedForCrossSiteInstanceMainFrameNavigation) {
+ const GURL url1("http://a.com/");
+ const GURL url2("http://b.com/");
+
+ NavigationControllerImpl& controller =
+ static_cast<NavigationControllerImpl&>(contents()->GetController());
+
+ // Navigate to `url1` and capture the entry's metrics, SiteInstance and
+ // document sequence number.
+ NavigationSimulator::NavigateAndCommitFromDocument(url1, main_test_rfh());
+ NavigationEntryImpl* entry1 = controller.GetEntryAtIndex(0);
+ BackForwardCacheMetrics* metrics1 = entry1->back_forward_cache_metrics();
+ SiteInstanceImpl* site_instance1 = entry1->site_instance();
+ int64_t dsn1 = entry1->root_node()->frame_entry->document_sequence_number();
+ ASSERT_TRUE(metrics1);
+ ASSERT_TRUE(site_instance1);
+
+ // Navigate cross-site to `url2`, which commits in a different SiteInstance.
+ NavigationSimulator::NavigateAndCommitFromDocument(url2, main_test_rfh());
+ NavigationEntryImpl* entry2 = controller.GetEntryAtIndex(1);
+ SiteInstanceImpl* site_instance2 = entry2->site_instance();
+ ASSERT_TRUE(site_instance2);
+ ASSERT_NE(site_instance1, site_instance2);
+ ASSERT_NE(entry2->back_forward_cache_metrics(), metrics1);
+
+ // A main-frame navigation that reports `entry1`'s document sequence number
+ // but commits in `entry2`'s SiteInstance must not reuse `entry1`'s metrics,
+ // since the document sequence number is renderer-supplied and a
+ // same-document navigation never changes SiteInstance.
+ scoped_refptr<BackForwardCacheMetrics> result = BackForwardCacheMetrics::
+ CreateOrReuseBackForwardCacheMetricsForNavigation(
+ entry1, /*is_main_frame_navigation=*/true, dsn1, site_instance2);
+ EXPECT_NE(result.get(), metrics1);
+
+ // A main-frame navigation reporting `entry1`'s document sequence number and
+ // committing in `entry1`'s SiteInstance should still reuse the metrics.
+ result = BackForwardCacheMetrics::
+ CreateOrReuseBackForwardCacheMetricsForNavigation(
+ entry1, /*is_main_frame_navigation=*/true, dsn1, site_instance1);
+ EXPECT_EQ(result.get(), metrics1);
+}
+
TEST_F(BackForwardCacheMetricsTest, PageWithFormsMetricsNotStore) {
DisableBackForwardCacheForTesting(contents(),
BackForwardCache::TEST_REQUIRES_NO_CACHING);
Original Bug Report
Cross-origin BackForwardCacheMetrics metadata leakage via forged document sequence number
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential logic flaw in Chromium’s Back/Forward Cache metrics allows a compromised renderer to bypass Site Isolation and leak cross-origin NotRestoredReasons. By forging a document sequence number (DSN) during navigation, an attacker’s renderer can potentially reuse a victim’s metrics object containing sensitive frame-tree information. This could expose URLs, iframe properties, and script source locations of the previously visited victim site to the attacker.
Affected files:
content/browser/back_forward_cache/back_forward_cache_metrics.cccontent/browser/renderer_host/navigation_controller_impl.cccontent/browser/renderer_host/navigation_request.cc
Estimated timestamp from git blame: 2019-04-02
Description
A potential vulnerability exists in the Back/Forward Cache metrics subsystem where a compromised renderer can bypass Site Isolation to access sensitive, cross-origin frame-tree metadata of a previously visited page.
In content/browser/back_forward_cache/back_forward_cache_metrics.cc:126-130, the function CreateOrReuseBackForwardCacheMetricsForNavigation is responsible for choosing whether to associate a new navigation with the previous entry’s BackForwardCacheMetrics object or create a new one. This decision is made solely based on the equality of a renderer-supplied document_sequence_number (DSN):
// content/browser/back_forward_cache/back_forward_cache_metrics.cc:126-130
if (!is_main_frame_navigation ||
committing_document_sequence_number ==
previous_entry_metrics->document_sequence_number_) {
return previous_entry_metrics;
}
Because the committing renderer controls the document_sequence_number parameter inside the DidCommitProvisionalLoad Mojo IPC, a compromised renderer could forge this sequence number. There is currently no SiteInstance or origin check in this metrics reuse path. While a sibling DSN consumer in content/browser/renderer_host/navigation_controller_impl.cc:5211-5214 was previously hardened with a SiteInstance validation to prevent forgery, this metrics reuse path remains unprotected.
Potential Attack Scenario
(Note: These are potential, suggested steps; our tooling does not currently run active exploits.)
- Victim Commit: The user loads
https://victim.com(which deliversCache-Control: no-storeto make it BFCache-ineligible). The browser associates aBackForwardCacheMetricsobject ($M_v$) with its navigation entry, setting the tracked DSN to $V$. - Navigation and Unload: The user navigates to the attacker’s page
https://attacker.com. During the unload of the victim page,UnloadOldFrame()runs first. Because the page is ineligible for BFCache,SetNotRestoredReasons()is called, populating $M_v$ with the victim’s complete frame-tree details (including subframe URLs, names, attributes, and JavaScript locations). - DSN Forgery: The compromised
attacker.comrenderer process estimates the victim’s DSN ($V$) by bounding the victim process’s startup time. Upon commit, the attacker’s renderer transmits a forgeddocument_sequence_numberequal to $V$ inside theDidCommitProvisionalLoadIPC parameters. - Metrics Hijacking: Because the forged DSN matches the last committed entry’s metrics DSN,
CreateOrReuseBackForwardCacheMetricsForNavigationreturns the victim’s metrics object ($M_v$) with no validation. This binds the victim’s populated metrics object to the attacker’sNavigationEntry($E_a$). - State Pinning: The attacker renderer performs a same-document navigation (such as a
history.pushState()), forging the DSN $V$ again. This creates a new entry $E_{a2}$ that shares $M_v$ and shifts the activenav_entry_idto $E_{a2}$. - Intermediate Navigation: The attacker navigates the tab to a BFCache-eligible page
https://attacker.com/thirdwith a normal DSN, putting the primary attacker page into the BFCache (keyed under $E_{a2}$). - BFCache-Bypass Restoration: The attacker’s script calls
history.go(-2)to target $E_a$. The BFCache lookup misses because the cached page is keyed under $E_{a2}$’s ID. This initiates a full cross-document history restoration navigation to $E_a$. - Data Leaked to Attacker: In
content/browser/renderer_host/navigation_request.cc:2282-2294, during the restoration navigation, the browser extracts the web-exposed reasons from the hijacked metrics object ($M_v$) and writes them tocommit_params_->not_restored_reasonswhich are then committed directly to the attacker’s renderer. The attacker reads the victim’s URL tree fromperformance.getEntriesByType('navigation')[0].notRestoredReasons.
Suggested Fix
To prevent this cross-origin metrics reuse, enforce a SiteInstance validation gate inside CreateOrReuseBackForwardCacheMetricsForNavigation (or its browser-process callers). For example, ensure that the metrics object is only reused if the previous_entry’s SiteInstance matches the SiteInstance associated with the committing navigation request.
// Suggested verification logic:
if (previous_entry->site_instance() &&
previous_entry->site_instance() != committing_site_instance) {
// Do not reuse metrics cross-origin
}
Evaluated with Chrome root at commit: 57b021e1fdae94a215627d29aeb1ccf2eb5b3e91
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.