CVE-2026-79060
Overview
Background
- `requestStorageAccessFor` (rsaFor) / top-level storage access
- A Storage Access API variant that lets a top-level site request cookie access on behalf of an embedded cross-site resource; the browser decides it in
TopLevelStorageAccessPermissionContext. - `net::SchemefulSite`
- Chromium’s “site” (scheme plus registrable domain) used to decide whether two origins are same-site or cross-site; storage-access grants only make sense across a cross-site boundary.
- Credentialless frame
- An iframe that loads without the user’s existing credentials (cookies) in an ephemeral context, so it must not be able to obtain the embedding user’s stored state.
- `bad_message::ReceivedBadMessage()`
- The browser-side mechanism that terminates a renderer process that sent an IPC a well-behaved renderer never would, used as a hardening response to compromised renderers.
Root Cause Analysis
In top_level_storage_access_permission_context.cc, the permission-decision routine (its name is not shown in the hunk; the regression test drives it through the DecidePermissionSync helper) took request_data->requesting_origin and request_data->embedding_origin from the renderer’s request and checked user gesture, transient activation and origin validity, but never rejected a request where both origins are the same site. It computed embedding_site and requesting_site only later, for the First-Party Sets metadata lookup. Per the commit message, well-behaved renderers never send same-site requests, but a compromised renderer could, and the browser would grant the permission; the commit message says that permission could then be read from a credentialless frame, crossing a boundary the browser was supposed to enforce.
The fix moves the SchemefulSite computation before the gesture/validity checks and adds an early branch: if requesting_site == embedding_site, it calls bad_message::ReceivedBadMessage(rfh->GetProcess(), TLSAPC_INVALID_PERMISSION_REQUEST_CONTEXT) and runs the callback with PermissionStatus::DENIED before returning. This tightens browser-side validation so the cross-site precondition is enforced in the trusted process rather than assumed from the renderer.
Attack Path
- Attacker controls a renderer The attacker must first have a compromised renderer process (e.g. via a separate renderer bug); per the commit message, ordinary web content cannot send this request.
- Forged same-site rsaFor request
The compromised renderer sends a top-level storage access permission request where
requesting_originandembedding_originare the same site, while satisfying the user-gesture and transient-activation checks. - Browser grants the permission
Pre-patch,
TopLevelStorageAccessPermissionContexthad no same-site check, so the request proceeded through the normal decision flow and could be granted. - Grant read from a credentialless frame The resulting permission could then be read from a credentialless frame, per the commit message, letting that frame obtain access it should not have (the exact downstream use is inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context.cc |
modified | |
TEST_Fchrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context_unittest.cc |
modified | |
TopLevelStorageAccessPermissionContextAPIWithFirstPartySetsTestchrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context_unittest.cc |
modified |
Files Changed
chrome/browser/top_level_storage_access_api/BUILD.gnchrome/browser/bad_message.htools/metrics/histograms/metadata/stability/enums.xmlchrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context_unittest.ccchrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context.cc
Audit Directions
- Renderer-asserted relationships in permission contextsReview other storage-access and permission contexts (e.g. the sibling Storage Access API context, which already has
SAGPC_INVALID_PERMISSION_REQUEST_CONTEXT) for conditions such as cross-site, top-level, or frame type that are assumed from renderer behavior instead of checked againstRenderFrameHoststate in the browser. - Credentialless and anonymous contextsCheck whether permission grants, content settings or storage keys keyed only by origin or site can be read from credentialless iframes, fenced frames or other partitioned contexts that should be excluded.
- Order of trust checksEnsure origin/site sanity checks on renderer-supplied
requesting_origin/embedding_originrun before any grant, prompt or asynchronous lookup, and that they are compared against browser-known frame origins.
Patch
From a3c67583da1402dba1ddd1242a9417f227e6e5b2 Mon Sep 17 00:00:00 2001
From: Chris Fredrickson <cfredric@chromium.org>
Date: Mon, 20 Jul 2026 07:57:52 -0700
Subject: [PATCH] Kill compromised same-site renderers in rsaFor permission context
Same-site permissions requests are never sent by well-behaved renderers,
since there is no privacy boundary to enforce there (generally
speaking). However, a compromised renderer could request (and get)
permission, and then read permission from a credentialless frame,
because the browser process did not properly enforce the cross-site
condition.
Fixed: 519254827
Change-Id: I232efae5ceda718ef0db01071cef1eae482b0afa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8110978
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: Alexei Svitkine <asvitkine@chromium.org>
Auto-Submit: Chris Fredrickson <cfredric@chromium.org>
Commit-Queue: Alexei Svitkine <asvitkine@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1664684}
---
diff --git a/chrome/browser/bad_message.h b/chrome/browser/bad_message.h
index 220ecd6..32c12993 100644
--- a/chrome/browser/bad_message.h
+++ b/chrome/browser/bad_message.h
@@ -34,6 +34,7 @@
RFH_INVALID_WEB_FRAME_URL = 11,
PVM_PRINT_FENCED_FRAME = 12,
SAGPC_INVALID_PERMISSION_REQUEST_CONTEXT = 13,
+ TLSAPC_INVALID_PERMISSION_REQUEST_CONTEXT = 14,
// Please add new elements here. The naming convention is abbreviated class
// name (e.g. RenderFrameHost becomes RFH) plus a unique description of the
diff --git a/chrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context.cc b/chrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context.cc
index aa49836..8122509e 100644
--- a/chrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context.cc
+++ b/chrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context.cc
@@ -10,6 +10,7 @@
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
+#include "chrome/browser/bad_message.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/first_party_sets/first_party_sets_policy_service.h"
#include "chrome/browser/first_party_sets/first_party_sets_policy_service_factory.h"
@@ -36,6 +37,7 @@
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom-shared.h"
+#include "third_party/blink/public/mojom/permissions/permission_status.mojom-shared.h"
namespace {
@@ -95,6 +97,22 @@
return;
}
+ net::SchemefulSite embedding_site(request_data->embedding_origin);
+ net::SchemefulSite requesting_site(request_data->requesting_origin);
+
+ if (requesting_site == embedding_site) {
+ // Well-behaved renderers don't send same-site permissions requests, since
+ // there is no privacy boundary within a site. This must be a compromised
+ // renderer.
+ bad_message::ReceivedBadMessage(
+ rfh->GetProcess(), bad_message::BadMessageReason::
+ TLSAPC_INVALID_PERMISSION_REQUEST_CONTEXT);
+ std::move(callback).Run(content::PermissionResult(
+ blink::mojom::PermissionStatus::DENIED,
+ content::PermissionStatusSource::UNSPECIFIED));
+ return;
+ }
+
if (!request_data->user_gesture || !rfh->HasTransientUserActivation() ||
!request_data->requesting_origin.is_valid() ||
!request_data->embedding_origin.is_valid()) {
@@ -111,9 +129,6 @@
return;
}
- net::SchemefulSite embedding_site(request_data->embedding_origin);
- net::SchemefulSite requesting_site(request_data->requesting_origin);
-
first_party_sets::FirstPartySetsPolicyServiceFactory::GetForBrowserContext(
browser_context())
->ComputeFirstPartySetMetadata(
diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml
index b6a79ff..2e12749 100644
--- a/tools/metrics/histograms/metadata/stability/enums.xml
+++ b/tools/metrics/histograms/metadata/stability/enums.xml
@@ -131,6 +131,7 @@
<int value="11" label="RFH_INVALID_WEB_FRAME_URL"/>
<int value="12" label="PVM_PRINT_FENCED_FRAME"/>
<int value="13" label="SAGPC_INVALID_PERMISSION_REQUEST_CONTEXT"/>
+ <int value="14" label="TLSAPC_INVALID_PERMISSION_REQUEST_CONTEXT"/>
</enum>
<enum name="BadMessageReasonContent">
diff --git a/chrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context_unittest.cc b/chrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context_unittest.cc
index c451452..2af14b8 100644
--- a/chrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context_unittest.cc
+++ b/chrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context_unittest.cc
@@ -24,6 +24,7 @@
#include "content/public/browser/permission_result.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
+#include "content/public/test/mock_render_process_host.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
@@ -233,6 +234,20 @@
.status);
}
+TEST_F(TopLevelStorageAccessPermissionContextTest, SameSiteDisallowed) {
+ TopLevelStorageAccessPermissionContext permission_context(profile());
+ NavigateAndCommit(GetTopLevelURL());
+
+ EXPECT_EQ(
+ DecidePermissionSync(&permission_context, /*user_gesture=*/true,
+ GetDummyEmbeddingUrl(), GetDummyEmbeddingUrl()),
+ PermissionStatus::DENIED);
+
+ EXPECT_EQ(1, static_cast<content::MockRenderProcessHost*>(
+ web_contents()->GetPrimaryMainFrame()->GetProcess())
+ ->bad_msg_count());
+}
+
class TopLevelStorageAccessPermissionContextAPIWithFirstPartySetsTest
: public TopLevelStorageAccessPermissionContextTest {
public:
diff --git a/chrome/browser/top_level_storage_access_api/BUILD.gn b/chrome/browser/top_level_storage_access_api/BUILD.gn
index 73a7a6de..fb5019a 100644
--- a/chrome/browser/top_level_storage_access_api/BUILD.gn
+++ b/chrome/browser/top_level_storage_access_api/BUILD.gn
@@ -15,6 +15,7 @@
]
deps = [
+ "//chrome/browser:bad_message",
"//chrome/browser/content_settings:content_settings_factory",
"//chrome/browser/first_party_sets",
"//chrome/common",
Regression Test / PoC
diff --git a/chrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context_unittest.cc b/chrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context_unittest.cc
index c451452..2af14b8 100644
--- a/chrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context_unittest.cc
+++ b/chrome/browser/top_level_storage_access_api/top_level_storage_access_permission_context_unittest.cc
@@ -24,6 +24,7 @@
#include "content/public/browser/permission_result.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
+#include "content/public/test/mock_render_process_host.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
@@ -233,6 +234,20 @@
.status);
}
+TEST_F(TopLevelStorageAccessPermissionContextTest, SameSiteDisallowed) {
+ TopLevelStorageAccessPermissionContext permission_context(profile());
+ NavigateAndCommit(GetTopLevelURL());
+
+ EXPECT_EQ(
+ DecidePermissionSync(&permission_context, /*user_gesture=*/true,
+ GetDummyEmbeddingUrl(), GetDummyEmbeddingUrl()),
+ PermissionStatus::DENIED);
+
+ EXPECT_EQ(1, static_cast<content::MockRenderProcessHost*>(
+ web_contents()->GetPrimaryMainFrame()->GetProcess())
+ ->bad_msg_count());
+}
+
class TopLevelStorageAccessPermissionContextAPIWithFirstPartySetsTest
: public TopLevelStorageAccessPermissionContextTest {
public: