Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Storage Access API
DescriptionInsufficient validation of untrusted input in Storage Access API
ComponentStorage Access API
Bug ClassLogic Error
Tracker498417152
Fix commit8f0e6af5fd0a (chromium/src) +43/-81
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/navigation_controller_impl.cc
modified
switch
content/browser/renderer_host/navigation_request.cc
modified

Files Changed

  • content/browser/fenced_frame/fenced_frame.cc
  • content/browser/loader/navigation_url_loader_impl.cc
  • content/browser/loader/navigation_url_loader_impl_unittest.cc
  • content/browser/loader/navigation_url_loader_unittest.cc
  • content/browser/renderer_host/navigation_controller_impl.cc
  • content/browser/renderer_host/navigation_controller_impl.h
  • content/browser/renderer_host/navigation_request.cc
From 8f0e6af5fd0aa413fc567e0d12f89c0e64ad8ee9 Mon Sep 17 00:00:00 2001
From: Chris Fredrickson <[email protected]>
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 <[email protected]>
Reviewed-by: Dave Tapuska <[email protected]>
Auto-Submit: Chris Fredrickson <[email protected]>
Commit-Queue: Chris Fredrickson <[email protected]>
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) ||
Loading diff…

Regression Test / PoC

shipped with the fix
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();
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • content/browser/storage_access/storage_access_handle.cc
  • content/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:

  1. Ensure a STORAGE_ACCESS content setting grant exists for attacker.com on site.com (e.g., via a prior legitimate use of the Storage Access API).
  2. A victim visits site.com, which embeds attacker.com in an iframe. The embedder explicitly forbids storage access via the header Permissions-Policy: storage-access=().
  3. The attacker compromises the renderer process for the attacker.com iframe.
  4. The compromised renderer triggers a same-origin self-navigation (e.g., to attacker.com/page2) and manually sets begin_params.storage_access_api_status = kAccessViaAPI in the BeginNavigation IPC.
  5. The browser processes the navigation and, lacking validation, accepts the spoofed status. The new document receives the kStorageAccessGrantEligible override.
  6. The compromised renderer requests to bind blink.mojom.StorageAccessHandle.
  7. 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.

View on issue tracker