CVE-2026-78976
Overview
Files Changed
content/browser/loader/navigation_url_loader_impl.cccontent/browser/loader/navigation_url_loader_impl_unittest.cc
Patch
From 7420bb9d35e52f33b267e8179fd78670826b9384 Mon Sep 17 00:00:00 2001 From: Chris Fredrickson <[email protected]> Date: Tue, 21 Jul 2026 15:25:26 -0700 Subject: [PATCH] Stop trusting renderer-supplied initiator_origin for Storage Access API The NavigationUrlLoaderImpl should use the browser-provided signal, rather than relying on the renderer's data. Fixed: 517550421 Change-Id: I56342ede633dfd0d4fe28329c3fa9de5c883e604 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8130881 Auto-Submit: Chris Fredrickson <[email protected]> Reviewed-by: Alex Moshchuk <[email protected]> Commit-Queue: Alex Moshchuk <[email protected]> Cr-Commit-Position: refs/heads/main@{#1665841} --- diff --git a/content/browser/loader/navigation_url_loader_impl.cc b/content/browser/loader/navigation_url_loader_impl.cc index e7e6814c..259a8e6d 100644 --- a/content/browser/loader/navigation_url_loader_impl.cc +++ b/content/browser/loader/navigation_url_loader_impl.cc @@ -338,9 +338,9 @@ const bool is_same_origin_initiator = request_info.begin_params->initiator_frame_token == frame_tree_node->current_frame_host()->GetFrameToken() && - request_info.common_params->initiator_origin && - request_info.common_params->initiator_origin->IsSameOriginWith( - request_info.common_params->url); + frame_tree_node->current_frame_host() + ->GetLastCommittedOrigin() + .IsSameOriginWith(request_info.common_params->url); new_request->storage_access_api_status = is_storage_access_grant_eligible && is_same_origin_initiator diff --git a/content/browser/loader/navigation_url_loader_impl_unittest.cc b/content/browser/loader/navigation_url_loader_impl_unittest.cc index ea30447..d877441 100644 --- a/content/browser/loader/navigation_url_loader_impl_unittest.cc +++ b/content/browser/loader/navigation_url_loader_impl_unittest.cc @@ -1587,6 +1587,7 @@ TestRenderFrameHost* rfh = static_cast<TestRenderFrameHost*>(web_contents_->GetPrimaryMainFrame()); + rfh->SetLastCommittedOriginForTesting(url::Origin::Create(url)); rfh->document_associated_data().PutCookieSettingOverride( net::CookieSettingOverride::kStorageAccessGrantEligible); @@ -1610,6 +1611,7 @@ TestRenderFrameHost* rfh = static_cast<TestRenderFrameHost*>(web_contents_->GetPrimaryMainFrame()); + rfh->SetLastCommittedOriginForTesting(url::Origin::Create(url)); rfh->document_associated_data().PutCookieSettingOverride( net::CookieSettingOverride::kStorageAccessGrantEligible); @@ -1632,6 +1634,8 @@ TestRenderFrameHost* rfh = static_cast<TestRenderFrameHost*>(web_contents_->GetPrimaryMainFrame()); + rfh->SetLastCommittedOriginForTesting( + url::Origin::Create(GURL("http://a.com"))); rfh->document_associated_data().PutCookieSettingOverride( net::CookieSettingOverride::kStorageAccessGrantEligible);
Regression Test / PoC
diff --git a/content/browser/loader/navigation_url_loader_impl_unittest.cc b/content/browser/loader/navigation_url_loader_impl_unittest.cc
index ea30447..d877441 100644
--- a/content/browser/loader/navigation_url_loader_impl_unittest.cc
+++ b/content/browser/loader/navigation_url_loader_impl_unittest.cc
@@ -1587,6 +1587,7 @@
TestRenderFrameHost* rfh =
static_cast<TestRenderFrameHost*>(web_contents_->GetPrimaryMainFrame());
+ rfh->SetLastCommittedOriginForTesting(url::Origin::Create(url));
rfh->document_associated_data().PutCookieSettingOverride(
net::CookieSettingOverride::kStorageAccessGrantEligible);
@@ -1610,6 +1611,7 @@
TestRenderFrameHost* rfh =
static_cast<TestRenderFrameHost*>(web_contents_->GetPrimaryMainFrame());
+ rfh->SetLastCommittedOriginForTesting(url::Origin::Create(url));
rfh->document_associated_data().PutCookieSettingOverride(
net::CookieSettingOverride::kStorageAccessGrantEligible);
@@ -1632,6 +1634,8 @@
TestRenderFrameHost* rfh =
static_cast<TestRenderFrameHost*>(web_contents_->GetPrimaryMainFrame());
+ rfh->SetLastCommittedOriginForTesting(
+ url::Origin::Create(GURL("http://a.com")));
rfh->document_associated_data().PutCookieSettingOverride(
net::CookieSettingOverride::kStorageAccessGrantEligible);
Original Bug Report
Potential Storage-Access Same-Origin Gate Bypass via Forgeable Initiator Origin
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: The Storage Access API (SAA) same-origin inheritance gate during navigations evaluates same-origin status using the renderer-supplied initiator origin instead of a browser-authoritative origin. A compromised renderer can forge this initiator origin within its site-granular process lock to inherit a sibling subdomain’s SAA grant. This potentially allows same-site cross-origin cookies to be sent on navigation requests, bypassing per-origin SAA boundaries and Permissions-Policy constraints.
Affected files:
content/browser/loader/navigation_url_loader_impl.cccontent/browser/renderer_host/navigation_request.cc
Estimated timestamp from git blame: 2023-02-21
Summary
There is a potential security bypass in the Storage Access API (SAA) same-origin navigation inheritance gate. When a navigation is committed, the browser determines whether to carry over a previous document’s per-frame SAA grant by evaluating the same-origin status of the navigation. However, the browser relies on the renderer-supplied common_params.initiator_origin to check if the initiator and target URLs are same-origin, rather than checking the browser-authoritative GetLastCommittedOrigin() of the initiator frame. Since initiator_origin is only verified via site-granular checks, a compromised renderer locked to a site (e.g., tracker.com) can forge initiator_origin as a same-site cross-origin sibling (e.g., https://b.tracker.com) when initiating a navigation from https://a.tracker.com. This potentially allows the compromised renderer to carry over a legitimate SAA grant from a.tracker.com to b.tracker.com, bypassing SAA boundaries.
Root Cause Analysis
In content/browser/renderer_host/navigation_request.cc, the decision of whether to inherit the SAA grant of the previous document is computed via ShouldLoadWithStorageAccess:
net::StorageAccessApiStatus ShouldLoadWithStorageAccess(
const blink::mojom::BeginNavigationParams& begin_params,
const blink::mojom::CommonNavigationParams& common_params,
const RenderFrameHostImpl* previous_document_rfh,
bool did_encounter_cross_origin_redirect,
const GURL response_url,
const network::mojom::URLResponseHead* response) {
...
if (!previous_document_rfh->document_associated_data()
.cookie_setting_overrides()
.Has(net::CookieSettingOverride::kStorageAccessGrantEligible)) {
return net::StorageAccessApiStatus::kNone;
}
if (begin_params.initiator_frame_token !=
previous_document_rfh->GetFrameToken()) {
return net::StorageAccessApiStatus::kNone;
}
if (!common_params.initiator_origin ||
!common_params.initiator_origin->IsSameOriginWith(response_url) ||
did_encounter_cross_origin_redirect) {
return net::StorageAccessApiStatus::kNone;
}
return net::StorageAccessApiStatus::kAccessViaAPI;
}
At the IPC boundary, common_params.initiator_origin is validated in VerifyInitiatorOrigin (content/browser/renderer_host/ipc_utils.cc line 77) by calling policy->HostsOrigin(process_id, initiator_origin). Because site isolation is site-level, subdomains of the same site (e.g., a.tracker.com and b.tracker.com) reside in the same process and share the tracker.com site lock. Hence, a compromised process can set initiator_origin to https://b.tracker.com, and it will pass HostsOrigin verification.
When ShouldLoadWithStorageAccess evaluates this request, it sees that previous_document_rfh (the frame for a.tracker.com which has the SAA grant) is eligible, and that the forged initiator_origin (https://b.tracker.com) is same-origin with the destination (https://b.tracker.com). As a result, b.tracker.com inherits the SAA grant of a.tracker.com.
Potential Steps to Reproduce
These are suggested/potential steps, as our testing tools do not currently have the ability to run arbitrary C++ or exploit code:
- Establish two subdomains of the same site:
https://a.tracker.comandhttps://b.tracker.comembedded in a third-party sitehttps://example.com. - Obtain a legitimate SAA grant for
https://a.tracker.com(e.g., viadocument.requestStorageAccess()), so that the frame’sdocument_associated_data().cookie_setting_overrides()containskStorageAccessGrantEligible. - Compromise the renderer process hosting
tracker.com(e.g., via a standard renderer memory corruption vulnerability). - In the compromised renderer, initiate a navigation from
https://a.tracker.comtohttps://b.tracker.comviaLocalFrameHost::BeginNavigation. - In the IPC arguments, set
common_params.initiator_origintohttps://b.tracker.comand setbegin_params.initiator_frame_tokento the frame token of the current frame. - Observe that the navigation commits at
https://b.tracker.comwithload_with_storage_access = trueandb.tracker.comobtains the SAA grant (settingkStorageAccessGrantEligible), gaining unpartitioned cookie access despite never having received an independent SAA grant or matching the embedder’s Permissions-Policy.
Impact
An attacker with a compromised renderer can carry over and abuse SAA grants belonging to same-site cross-origin subdomains, bypassing Permissions-Policy boundaries and the same-origin gate.
Suggested Fix
The browser must validate the same-origin status using the browser-authoritative last committed origin of the initiator frame instead of relying on the renderer-supplied initiator_origin.
Compare the target URL with the authoritative initiator frame’s origin:
previous_document_rfh->GetLastCommittedOrigin().IsSameOriginWith(response_url)
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
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.