CVE-2026-11255
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/navigation_controller_impl.cc |
modified | |
switchcontent/browser/renderer_host/navigation_request.cc |
modified |
Files Changed
content/browser/fenced_frame/fenced_frame.cccontent/browser/loader/navigation_url_loader_impl.cccontent/browser/loader/navigation_url_loader_impl_unittest.cccontent/browser/loader/navigation_url_loader_unittest.cccontent/browser/renderer_host/navigation_controller_impl.cccontent/browser/renderer_host/navigation_controller_impl.hcontent/browser/renderer_host/navigation_request.cc
Patch
From 8f0e6af5fd0aa413fc567e0d12f89c0e64ad8ee9 Mon Sep 17 00:00:00 2001
From: Chris Fredrickson <cfredric@chromium.org>
Date: Thu, 09 Apr 2026 10:18:34 -0700
Subject: [PATCH] [SAA] Remove mojo navigation params that convey renderer's SAA status
These params are now unnecessary (after https://crrev.com/c/6494679),
and were never trustworthy anyway. Instead of asking the renderer to
provide this state, we should just use the browser's copy of it.
This removal cleans up some complexity w.r.t. local/remote frames.
Fixed: 498417152
Change-Id: Ic0cb9e9d349366237bf86976578d4a22bb5c4f41
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7733321
Reviewed-by: Arthur Sonzogni <arthursonzogni@chromium.org>
Reviewed-by: Dave Tapuska <dtapuska@chromium.org>
Auto-Submit: Chris Fredrickson <cfredric@chromium.org>
Commit-Queue: Chris Fredrickson <cfredric@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1612331}
---
diff --git a/content/browser/fenced_frame/fenced_frame.cc b/content/browser/fenced_frame/fenced_frame.cc
index f8e700ee..651302a 100644
--- a/content/browser/fenced_frame/fenced_frame.cc
+++ b/content/browser/fenced_frame/fenced_frame.cc
@@ -187,7 +187,6 @@
/*is_unfenced_top_navigation=*/false,
/*force_new_browsing_instance=*/true, /*is_container_initiated=*/false,
/*has_rel_opener=*/false,
- /*storage_access_api_status=*/net::StorageAccessApiStatus::kNone,
embedder_shared_storage_context);
}
diff --git a/content/browser/loader/navigation_url_loader_impl.cc b/content/browser/loader/navigation_url_loader_impl.cc
index 6d7af1b..ffd754c9 100644
--- a/content/browser/loader/navigation_url_loader_impl.cc
+++ b/content/browser/loader/navigation_url_loader_impl.cc
@@ -96,6 +96,7 @@
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "net/ssl/ssl_info.h"
+#include "net/storage_access_api/status.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/url_request/redirect_util.h"
#include "services/metrics/public/cpp/metrics_utils.h"
@@ -331,7 +332,12 @@
}
new_request->storage_access_api_status =
- request_info.begin_params->storage_access_api_status;
+ frame_tree_node->current_frame_host()
+ ->document_associated_data()
+ .cookie_setting_overrides()
+ .Has(net::CookieSettingOverride::kStorageAccessGrantEligible)
+ ? net::StorageAccessApiStatus::kAccessViaAPI
+ : net::StorageAccessApiStatus::kNone;
WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(
WebContents::FromFrameTreeNodeId(frame_tree_node->frame_tree_node_id()));
diff --git a/content/browser/loader/navigation_url_loader_impl_unittest.cc b/content/browser/loader/navigation_url_loader_impl_unittest.cc
index 3a50e2cd..f929236 100644
--- a/content/browser/loader/navigation_url_loader_impl_unittest.cc
+++ b/content/browser/loader/navigation_url_loader_impl_unittest.cc
@@ -158,7 +158,7 @@
base::TimeTicks() /* before_unload_dialog_closed */,
false /* started_with_transient_activation */,
false /* started_by_ad */, false /* is_container_initiated */,
- net::StorageAccessApiStatus::kNone, false /* has_rel_opener */);
+ false /* has_rel_opener */);
auto common_params = blink::CreateCommonNavigationParams();
common_params->url = url;
diff --git a/content/browser/loader/navigation_url_loader_unittest.cc b/content/browser/loader/navigation_url_loader_unittest.cc
index 236ffe6..486d834 100644
--- a/content/browser/loader/navigation_url_loader_unittest.cc
+++ b/content/browser/loader/navigation_url_loader_unittest.cc
@@ -101,7 +101,7 @@
base::TimeTicks() /* before_unload_dialog_closed */,
false /* started_with_transient_activation */,
false /* started_by_ad */, false /* is_container_initiated */,
- net::StorageAccessApiStatus::kNone, false /* has_rel_opener */);
+ false /* has_rel_opener */);
auto common_params = blink::CreateCommonNavigationParams();
common_params->url = url;
common_params->initiator_origin = url::Origin::Create(url);
diff --git a/content/browser/renderer_host/navigation_controller_impl.cc b/content/browser/renderer_host/navigation_controller_impl.cc
index 1e9c5a4..971a685 100644
--- a/content/browser/renderer_host/navigation_controller_impl.cc
+++ b/content/browser/renderer_host/navigation_controller_impl.cc
@@ -3262,7 +3262,6 @@
bool force_new_browsing_instance,
bool is_container_initiated,
bool has_rel_opener,
- net::StorageAccessApiStatus storage_access_api_status,
std::optional<std::u16string> embedder_shared_storage_context) {
if (is_renderer_initiated) {
DCHECK(initiator_origin.has_value());
@@ -3393,7 +3392,7 @@
/*from_frame_proxy=*/true,
is_embedder_initiated_fenced_frame_navigation,
is_unfenced_top_navigation, is_container_initiated,
- storage_access_api_status, embedder_shared_storage_context);
+ embedder_shared_storage_context);
if (!request) {
return;
@@ -4496,7 +4495,6 @@
bool is_embedder_initiated_fenced_frame_navigation,
bool is_unfenced_top_navigation,
bool is_container_initiated,
- net::StorageAccessApiStatus storage_access_api_status,
std::optional<std::u16string> embedder_shared_storage_context) {
DCHECK_EQ(-1, GetIndexOfEntry(entry));
@@ -4720,8 +4718,7 @@
params.impression, started_with_transient_activation,
params.started_by_ad, params.is_pdf,
is_embedder_initiated_fenced_frame_navigation, is_container_initiated,
- params.has_rel_opener, storage_access_api_status,
- embedder_shared_storage_context);
+ params.has_rel_opener, embedder_shared_storage_context);
if (!navigation_request) {
return nullptr;
diff --git a/content/browser/renderer_host/navigation_controller_impl.h b/content/browser/renderer_host/navigation_controller_impl.h
index df828bb..3eb808d 100644
--- a/content/browser/renderer_host/navigation_controller_impl.h
+++ b/content/browser/renderer_host/navigation_controller_impl.h
@@ -250,8 +250,6 @@
bool force_new_browsing_instance = false,
bool is_container_initiated = false,
bool has_rel_opener = false,
- net::StorageAccessApiStatus storage_access_api_status =
- net::StorageAccessApiStatus::kNone,
std::optional<std::u16string> embedder_shared_storage_context =
std::nullopt);
@@ -778,8 +776,6 @@
bool is_embedder_initiated_fenced_frame_navigation = false,
bool is_unfenced_top_navigation = false,
bool is_container_initiated = false,
- net::StorageAccessApiStatus storage_access_api_status =
- net::StorageAccessApiStatus::kNone,
std::optional<std::u16string> embedder_shared_storage_context =
std::nullopt);
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index 722a1e2b..a9b8e82 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -159,6 +159,7 @@
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/url_util.h"
#include "net/cookies/cookie_access_result.h"
+#include "net/cookies/cookie_setting_override.h"
#include "net/filter/source_stream_type.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
@@ -1192,28 +1193,28 @@
// Storage Access API: https://privacycg.github.io/storage-access/#navigation
//
- // If a document has storage access, and initiates a navigation in the same
- // frame toward a document from the same origin, the `has storage access` bit
- // is inherited.
- //
- // This doesn't hold if there is a cross-origin redirect in between.
- //
- // Note: `begin_params` and `common_params` are not trusted, so we have to
- // check the frame token.
- switch (begin_params.storage_access_api_status) {
- case net::StorageAccessApiStatus::kNone:
- return net::StorageAccessApiStatus::kNone;
- case net::StorageAccessApiStatus::kAccessViaAPI:
- return common_params.initiator_origin &&
- common_params.initiator_origin->IsSameOriginWith(
- response_url) &&
- begin_params.initiator_frame_token &&
- begin_params.initiator_frame_token ==
- previous_document_rfh->GetFrameToken() &&
- !did_encounter_cross_origin_redirect
- ? begin_params.storage_access_api_status
- : net::StorageAccessApiStatus::kNone;
+ // If a document has storage access, and initiates a same-origin navigation in
+ // the same frame toward a document from the same origin, the `has storage
+ // access` bit is inherited.
+ if (!previous_document_rfh->document_associated_data()
+ .cookie_setting_overrides()
+ .Has(net::CookieSettingOverride::kStorageAccessGrantEligible)) {
+ // Frame was missing the grant eligible override, so there's no access to
+ // carry over.
+ return net::StorageAccessApiStatus::kNone;
}
+ if (begin_params.initiator_frame_token !=
+ previous_document_rfh->GetFrameToken()) {
+ // Navigation was not self-initiated.
+ return net::StorageAccessApiStatus::kNone;
+ }
+ if (!common_params.initiator_origin ||
+ !common_params.initiator_origin->IsSameOriginWith(response_url) ||
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 3a50e2cd..f929236 100644
--- a/content/browser/loader/navigation_url_loader_impl_unittest.cc
+++ b/content/browser/loader/navigation_url_loader_impl_unittest.cc
@@ -158,7 +158,7 @@
base::TimeTicks() /* before_unload_dialog_closed */,
false /* started_with_transient_activation */,
false /* started_by_ad */, false /* is_container_initiated */,
- net::StorageAccessApiStatus::kNone, false /* has_rel_opener */);
+ false /* has_rel_opener */);
auto common_params = blink::CreateCommonNavigationParams();
common_params->url = url;
diff --git a/content/browser/loader/navigation_url_loader_unittest.cc b/content/browser/loader/navigation_url_loader_unittest.cc
index 236ffe6..486d834 100644
--- a/content/browser/loader/navigation_url_loader_unittest.cc
+++ b/content/browser/loader/navigation_url_loader_unittest.cc
@@ -101,7 +101,7 @@
base::TimeTicks() /* before_unload_dialog_closed */,
false /* started_with_transient_activation */,
false /* started_by_ad */, false /* is_container_initiated */,
- net::StorageAccessApiStatus::kNone, false /* has_rel_opener */);
+ false /* has_rel_opener */);
auto common_params = blink::CreateCommonNavigationParams();
common_params->url = url;
common_params->initiator_origin = url::Origin::Create(url);
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index f20a708..a9f9a8f 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -2434,7 +2434,7 @@
base::TimeTicks() /* before_unload_dialog_closed */,
false /* started_with_transient_activation */,
false /* started_by_ad */, false /* is_container_initiated */,
- net::StorageAccessApiStatus::kNone, false /* has_rel_opener */);
+ false /* has_rel_opener */);
// Receiving the invalid IPC message should lead to renderer process
// termination.
@@ -2521,7 +2521,7 @@
base::TimeTicks() /* before_unload_dialog_closed */,
false /* started_with_transient_activation */,
false /* started_by_ad */, false /* is_container_initiated */,
- net::StorageAccessApiStatus::kNone, false /* has_rel_opener */);
+ false /* has_rel_opener */);
// Receiving the invalid IPC message should lead to renderer process
// termination.
diff --git a/content/test/navigation_simulator_impl.cc b/content/test/navigation_simulator_impl.cc
index 8d72dd2..0ecd97d9 100644
--- a/content/test/navigation_simulator_impl.cc
+++ b/content/test/navigation_simulator_impl.cc
@@ -1393,8 +1393,7 @@
base::TimeTicks() /* before_unload_dialog_opened */,
base::TimeTicks() /* before_unload_dialog_closed */,
has_user_gesture_, false /* started_by_ad */,
- false /* is_container_initiated */,
- net::StorageAccessApiStatus::kNone, false /* has_rel_opener */);
+ false /* is_container_initiated */, false /* has_rel_opener */);
auto common_params = blink::CreateCommonNavigationParams();
common_params->navigation_start =
navigation_start_.is_null() ? base::TimeTicks::Now() : navigation_start_;
diff --git a/content/test/test_render_frame_host.cc b/content/test/test_render_frame_host.cc
index 584d417..b44560b7 100644
--- a/content/test/test_render_frame_host.cc
+++ b/content/test/test_render_frame_host.cc
@@ -470,7 +470,7 @@
base::TimeTicks() /* before_unload_dialog_closed */,
false /* started_with_transient_activation */,
false /* started_by_ad */, false /* is_container_initiated */,
- net::StorageAccessApiStatus::kNone, false /* has_rel_opener */);
+ false /* has_rel_opener */);
auto common_params = blink::CreateCommonNavigationParams();
common_params->url = url;
common_params->initiator_origin = GetLastCommittedOrigin();
Original Bug Report
Permissions-Policy bypass for Storage Access via spoofed navigation status
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 without the security team.
Overview: A compromised renderer can bypass Permissions-Policy: storage-access=() by spoofing its storage access status during a self-navigation. The browser fails to verify if the previous document actually possessed the storage access grant before allowing the new document to inherit the status.
Affected files:
content/browser/renderer_host/navigation_request.cccontent/browser/storage_access/storage_access_handle.cccontent/browser/renderer_host/render_frame_host_impl.cc
Estimated timestamp from git blame: 2024-07-09
Vulnerability Details
A logic flaw exists in NavigationRequest::ShouldLoadWithStorageAccess that allows a compromised renderer to bypass the Permissions-Policy check for the Storage Access API.
When a renderer initiates a navigation, it can specify a storage_access_api_status in the BeginNavigationParams IPC. The browser process allows inheritance of the ‘storage access’ bit during same-origin self-navigations to preserve state. However, NavigationRequest::ShouldLoadWithStorageAccess (in content/browser/renderer_host/navigation_request.cc) only validates the initiator’s identity and origin:
case net::StorageAccessApiStatus::kAccessViaAPI:
return common_params.initiator_origin &&
common_params.initiator_origin->IsSameOriginWith(
response_url) &&
begin_params.initiator_frame_token &&
begin_params.initiator_frame_token ==
previous_document_rfh->GetFrameToken() &&
!did_encounter_cross_origin_redirect
? begin_params.storage_access_api_status
: net::StorageAccessApiStatus::kNone;
It crucially fails to verify whether the previous_document_rfh actually possessed the kStorageAccessGrantEligible override.
If a compromised renderer performs a same-origin self-navigation and maliciously sets storage_access_api_status to kAccessViaAPI, the browser accepts this status. When the navigation commits, RenderFrameHostImpl::SetStorageAccessApiStatus applies the kStorageAccessGrantEligible override to the new document’s associated data.
With this override in place, the renderer can successfully bind blink.mojom.StorageAccessHandle via the BrowserInterfaceBroker. StorageAccessHandle::Create checks IsFullCookieAccessAllowed(), which delegates to CookieSettingsBase::IsAllowedByStorageAccessGrant. As noted in the comments of IsAllowedByStorageAccessGrant, the presence of the kStorageAccessGrantEligible override implies that the browser has already verified the Permissions-Policy. Thus, it completely bypasses the Permissions-Policy check, granting the renderer unpartitioned access to first-party storage (IndexedDB, CacheStorage, OPFS, etc.) if a prior content setting grant exists.
Potential Exploitation Steps
Note: Our tooling agent does not currently run code, but the following steps trace the required exploit path:
- Ensure a
STORAGE_ACCESScontent setting grant exists forattacker.comonsite.com(e.g., via a prior legitimate use of the Storage Access API). - A victim visits
site.com, which embedsattacker.comin an iframe. The embedder explicitly forbids storage access via the headerPermissions-Policy: storage-access=(). - The attacker compromises the renderer process for the
attacker.comiframe. - The compromised renderer triggers a same-origin self-navigation (e.g., to
attacker.com/page2) and manually setsbegin_params.storage_access_api_status = kAccessViaAPIin theBeginNavigationIPC. - The browser processes the navigation and, lacking validation, accepts the spoofed status. The new document receives the
kStorageAccessGrantEligibleoverride. - The compromised renderer requests to bind
blink.mojom.StorageAccessHandle. - The browser grants the handle, allowing the attacker unpartitioned access to their first-party storage and bypassing the embedder’s explicitly restrictive
Permissions-Policy.
Suggested Fix
Modify NavigationRequest::ShouldLoadWithStorageAccess to verify that the previous document actually had the storage access override before allowing it to be inherited. For example:
case net::StorageAccessApiStatus::kAccessViaAPI:
return common_params.initiator_origin &&
common_params.initiator_origin->IsSameOriginWith(
response_url) &&
begin_params.initiator_frame_token &&
begin_params.initiator_frame_token ==
previous_document_rfh->GetFrameToken() &&
!did_encounter_cross_origin_redirect &&
previous_document_rfh->GetCookieSettingOverrides().Has(
net::CookieSettingOverride::kStorageAccessGrantEligible)
? begin_params.storage_access_api_status
: net::StorageAccessApiStatus::kNone;
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results 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.