CVE-2026-78976
Overview
Background
- Storage Access API (SAA)
- a web API that lets an embedded cross-site document, after a successful
document.requestStorageAccess(), use its unpartitioned (third-party) cookies; the browser records this on the frame as thenet::CookieSettingOverride::kStorageAccessGrantEligibleoverride. - `storage_access_api_status`
- a field on the outgoing
network::ResourceRequest. When it is set, the network layer may attach unpartitioned cookies to the navigation request using the frame’s storage-access grant. - `initiator_origin`
- the origin that starts a navigation, carried in
request_info.common_params. For renderer-initiated navigations it is supplied by the renderer, so a compromised renderer can choose its value. - `GetLastCommittedOrigin()`
- the origin of the document currently committed in a
RenderFrameHost, as recorded by the browser itself; this is the trustworthy source of the frame’s origin.
Root Cause Analysis
In navigation_url_loader_impl.cc, the code that builds a navigation’s network::ResourceRequest (the enclosing function is not named in the hunk) only lets a self-initiated navigation carry the frame’s storage-access grant when is_storage_access_grant_eligible && is_same_origin_initiator. Before the patch, is_same_origin_initiator required initiator_frame_token to equal the current frame’s token and then trusted the renderer-supplied request_info.common_params->initiator_origin to be same-origin with the destination url. The invariant SAA depends on is that a grant may only follow a navigation from the frame’s real origin to the same origin. Because the browser trusted the renderer’s claim of who started the navigation, a renderer that lied about initiator_origin could pass the check.
The fix replaces the renderer value with frame_tree_node->current_frame_host()->GetLastCommittedOrigin(), which the browser tracks, so the same-origin comparison is between the frame’s actual committed origin and the target URL. The updated unit tests set SetLastCommittedOriginForTesting so the existing positive and negative cases still pass under the new browser-side check.
initiator_origin. The fix uses the browser’s own record of the frame’s committed origin instead.Attack Path
- Obtain a storage-access grant
An embedded cross-site frame gets an SAA grant, for example through
document.requestStorageAccess()with the required user interaction and permission, so itsRenderFrameHosthaskStorageAccessGrantEligibleindocument_associated_data(). - Control the renderer
The attacker has a compromised renderer process. Changing
common_params->initiator_originis not possible from ordinary JavaScript; the commit title explicitly calls this value “renderer-supplied”. - Self-navigate with a forged initiator
The frame starts a navigation of itself (so
initiator_frame_tokenmatches the current frame), withinitiator_originset to match a destination origin that differs from the frame’s real committed origin. - Grant carried to the wrong origin
Before the patch,
is_same_origin_initiatorwas true, sostorage_access_api_statuswas set and the navigation to the other origin could be sent with unpartitioned cookies that the grant should not have covered (inferred; which destinations can actually be reached depends on browser-side checks ofinitiator_originthat the patch does not show).
Impact Assessment
Files Changed
content/browser/loader/navigation_url_loader_impl_unittest.cccontent/browser/loader/navigation_url_loader_impl.cc
Audit Directions
- Renderer-supplied `initiator_origin` in privacy or security gatesSearch the browser process for other decisions keyed on
common_params->initiator_originorbegin_paramsfields (cookie partitioning, SameSite handling, Fetch Metadata, permission inheritance). Where a browser-sideGetLastCommittedOrigin()or the initiatorRenderFrameHost’s origin is available, prefer it. - Mixed trusted and untrusted conditionsIn this check
initiator_frame_tokenwas compared with a browser-side token, but the origin half was not. Look for compound conditions where one renderer-supplied part can be forged on its own. - Grant propagation across navigationsReview how other per-document grants (
CookieSettingOverridevalues, top-level storage access, FedCM-based cookie access) are carried into the next document, and check that each hop’s origin is checked against browser-tracked state.
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.