Medium watch CVSS 4.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in StorageAccessAPI
DescriptionImproper input validation in StorageAccessAPI
ComponentStorageAccessAPI
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517550421
Fix commit7420bb9d35e5 (chromium/src) +7/-3
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-07-21 (public git)
Disclosed2026-08-25
Patch gap35 days — fix public in source before the advisory

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 the net::CookieSettingOverride::kStorageAccessGrantEligible override.
`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.

Key insight
A security decision (whether to carry an unpartitioned-cookie grant across a navigation) was made from renderer-controlled data, initiator_origin. The fix uses the browser’s own record of the frame’s committed origin instead.

Attack Path

  1. 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 its RenderFrameHost has kStorageAccessGrantEligible in document_associated_data().
  2. Control the renderer The attacker has a compromised renderer process. Changing common_params->initiator_origin is not possible from ordinary JavaScript; the commit title explicitly calls this value “renderer-supplied”.
  3. Self-navigate with a forged initiator The frame starts a navigation of itself (so initiator_frame_token matches the current frame), with initiator_origin set to match a destination origin that differs from the frame’s real committed origin.
  4. Grant carried to the wrong origin Before the patch, is_same_origin_initiator was true, so storage_access_api_status was 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 of initiator_origin that the patch does not show).

Impact Assessment

A compromised renderer holding an SAA grant could extend that grant to navigations to an origin other than its own, so unpartitioned (third-party) cookies would be sent where third-party cookie blocking should have withheld them. This weakens the SAA and third-party-cookie privacy boundary (inferred). The preconditions are renderer compromise, an existing storage-access grant on the frame, and a self-initiated navigation. The advisory classifies this as “Improper input validation in StorageAccessAPI” and does not report in-the-wild exploitation.

Files Changed

  • content/browser/loader/navigation_url_loader_impl_unittest.cc
  • content/browser/loader/navigation_url_loader_impl.cc

Audit Directions

  • Renderer-supplied `initiator_origin` in privacy or security gates
    Search the browser process for other decisions keyed on common_params->initiator_origin or begin_params fields (cookie partitioning, SameSite handling, Fetch Metadata, permission inheritance). Where a browser-side GetLastCommittedOrigin() or the initiator RenderFrameHost’s origin is available, prefer it.
  • Mixed trusted and untrusted conditions
    In this check initiator_frame_token was 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 navigations
    Review how other per-document grants (CookieSettingOverride values, 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.
From 7420bb9d35e52f33b267e8179fd78670826b9384 Mon Sep 17 00:00:00 2001
From: Chris Fredrickson <cfredric@chromium.org>
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 <cfredric@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Commit-Queue: Alex Moshchuk <alexmos@chromium.org>
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);
 
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 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);
Loading diff…

Original Bug Report

reported by vm...@google.com

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

  1. Establish two subdomains of the same site: https://a.tracker.com and https://b.tracker.com embedded in a third-party site https://example.com.
  2. Obtain a legitimate SAA grant for https://a.tracker.com (e.g., via document.requestStorageAccess()), so that the frame’s document_associated_data().cookie_setting_overrides() contains kStorageAccessGrantEligible.
  3. Compromise the renderer process hosting tracker.com (e.g., via a standard renderer memory corruption vulnerability).
  4. In the compromised renderer, initiate a navigation from https://a.tracker.com to https://b.tracker.com via LocalFrameHost::BeginNavigation.
  5. In the IPC arguments, set common_params.initiator_origin to https://b.tracker.com and set begin_params.initiator_frame_token to the frame token of the current frame.
  6. Observe that the navigation commits at https://b.tracker.com with load_with_storage_access = true and b.tracker.com obtains the SAA grant (setting kStorageAccessGrantEligible), 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.

View on issue tracker