Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Network
DescriptionIncorrect authorization in Network
ComponentNetwork
Bug ClassLogic Error
Tracker518065628
Fix commit215b9995626a (chromium/src) +127/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
content/browser/loader/navigation_url_loader_impl.cc
modified

Files Changed

  • content/browser/content_security_policy_browsertest.cc
  • content/browser/loader/navigation_url_loader_impl.cc
From 215b9995626a28eb38b01582a4b76cfc9e4f93a8 Mon Sep 17 00:00:00 2001
From: Keita Suzuki <[email protected]>
Date: Mon, 06 Jul 2026 02:11:03 -0700
Subject: [PATCH] Re-evaluate sandbox SameSite=None cookie override on redirect

NavigationURLLoaderImpl computed the allow-same-site-none-cookies
sandbox cookie-setting override once in the constructor, using the
navigation's tentative origin derived from the initial request URL, and
baked it into the navigation URLLoaderFactory. On a server redirect the
existing factory and loader were reused so the override could remain in
effect even though the post-redirect origin would no longer pass the
SchemeHostPort-exact ancestor check.

Track the override value used to build the network factory and recompute
it in FollowRedirect() once the request URL has been updated. If the
value changes, recreate the network factory with the new value and clear
default_loader_used_ so Restart() resets the loader and the redirected
request is issued through the new factory. This is a no-op for
navigations that never had the override.

Add content_browsertests covering a sandboxed a.test iframe whose
grandchild navigates to a.test and is redirected to sub.a.test under
third-party-cookie blocking (verifying the override is not applied),
as well as when redirected to another valid URL on a.test (verifying
the override is preserved).

TAG=agy
CONV=3216e3b4-2222-4094-83a1-01bcd0318e0a

Bug: 518065628
Change-Id: I316ce02b5e387e78312eb46ceb1903151cc22d8e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8033640
Commit-Queue: Keita Suzuki <[email protected]>
Reviewed-by: Rakina Zata Amni <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1657046}
---

diff --git a/content/browser/content_security_policy_browsertest.cc b/content/browser/content_security_policy_browsertest.cc
index 76d835f72..fdcb1922 100644
--- a/content/browser/content_security_policy_browsertest.cc
+++ b/content/browser/content_security_policy_browsertest.cc
@@ -11,6 +11,7 @@
 #include "base/memory/raw_ref.h"
 #include "base/notreached.h"
 #include "base/path_service.h"
+#include "base/strings/escape.h"
 #include "base/strings/strcat.h"
 #include "base/strings/stringprintf.h"
 #include "base/test/scoped_feature_list.h"
@@ -447,6 +448,7 @@
 namespace {
 
 constexpr std::string_view kHostA = "a.test";
+constexpr std::string_view kHostSubA = "sub.a.test";
 constexpr std::string_view kHostB = "b.test";
 
 constexpr std::string_view kTopLevelPath = "/top-level.html";
@@ -795,4 +797,94 @@
   EXPECT_EQ(FetchWithCredentials(grandchild_iframe, grandchild_iframe_url), "");
 }
 
+IN_PROC_BROWSER_TEST_P(AllowSameSiteNoneCookiesContentSecurityPolicyBrowserTest,
+                       NestedIframeNavigationCrossOriginRedirect) {
+  GURL top_level = https_server()->GetURL(kHostA, kTopLevelPath);
+  GURL middle_iframe_url = https_server()->GetURL(kHostA, kCrossSiteIframePath);
+  GURL grandchild_target_url =
+      https_server()->GetURL(kHostSubA, kCrossSiteIframePath);
+  GURL grandchild_redirect_url = https_server()->GetURL(
+      kHostA, "/server-redirect?" +
+                  base::EscapeQueryParamValue(grandchild_target_url.spec(),
+                                              /*use_plus=*/false));
+
+  ASSERT_TRUE(SetCookie(web_contents()->GetBrowserContext(),
+                        https_server()->GetURL(kHostSubA, kTopLevelPath),
+                        "foo=bar;SameSite=None;Secure;"));
+
+  // Top a.test embeds a sandboxed a.test iframe which then embeds an inner
+  // iframe that navigates to a.test and is server-redirected to sub.a.test.
+  ASSERT_TRUE(NavigateToURL(shell(), top_level));
+  ASSERT_TRUE(
+      ExecJs(web_contents()->GetPrimaryMainFrame(),
+             JsReplace(R"(document.body.innerHTML =
+                    '<iframe id="middle" src=$1 sandbox=$2></iframe>';)",
+                       middle_iframe_url.spec(), sandbox_iframe_policy())));
+  WaitForLoadStop(web_contents());
+  RenderFrameHost* middle_iframe = ChildFrameAt(shell(), 0);
+
+  ASSERT_TRUE(ExecJs(middle_iframe, JsReplace(R"(document.body.innerHTML =
+                    '<iframe id="grandchild" src=$1></iframe>';)",
+                                              grandchild_redirect_url.spec())));
+  WaitForLoadStop(web_contents());
+  RenderFrameHost* grandchild_iframe = ChildFrameAt(middle_iframe, 0);
+  ASSERT_TRUE(grandchild_iframe);
+  ASSERT_EQ(grandchild_iframe->GetLastCommittedURL(), grandchild_target_url);
+
+  // The override does not apply to the redirected navigation request because
+  // the grandchild's origin no longer matches each ancestor's origin (or
+  // precursor) after the redirect to sub.a.test.
+  EXPECT_FALSE(grandchild_iframe->GetCookieSettingOverrides().Has(
+      net::CookieSettingOverride::kAllowSameSiteNoneCookiesInSandbox));
+  EXPECT_EQ(EvalJs(grandchild_iframe, "document.body.textContent"), "");
+  EXPECT_EQ(FetchWithCredentials(grandchild_iframe, grandchild_target_url), "");
+}
+
+IN_PROC_BROWSER_TEST_P(AllowSameSiteNoneCookiesContentSecurityPolicyBrowserTest,
+                       NestedIframeNavigationSameOriginRedirect) {
+  GURL top_level = https_server()->GetURL(kHostA, kTopLevelPath);
+  GURL middle_iframe_url = https_server()->GetURL(kHostA, kCrossSiteIframePath);
+  GURL grandchild_target_url =
+      https_server()->GetURL(kHostA, kCrossSiteIframePath);
+  GURL grandchild_redirect_url = https_server()->GetURL(
+      kHostA, "/server-redirect?" +
+                  base::EscapeQueryParamValue(grandchild_target_url.spec(),
+                                              /*use_plus=*/false));
+
+  ASSERT_TRUE(SetCookie(web_contents()->GetBrowserContext(),
+                        https_server()->GetURL(kHostA, kTopLevelPath),
+                        "foo=bar;SameSite=None;Secure;"));
+
+  // Top a.test embeds a sandboxed a.test iframe which then embeds an inner
+  // iframe that navigates to a.test and is server-redirected to another
+  // a.test URL.
+  ASSERT_TRUE(NavigateToURL(shell(), top_level));
+  ASSERT_TRUE(
+      ExecJs(web_contents()->GetPrimaryMainFrame(),
+             JsReplace(R"(document.body.innerHTML =
+                    '<iframe id="middle" src=$1 sandbox=$2></iframe>';)",
+                       middle_iframe_url.spec(), sandbox_iframe_policy())));
+  WaitForLoadStop(web_contents());
+  RenderFrameHost* middle_iframe = ChildFrameAt(shell(), 0);
+
+  ASSERT_TRUE(ExecJs(middle_iframe, JsReplace(R"(document.body.innerHTML =
+                    '<iframe id="grandchild" src=$1></iframe>';)",
+                                              grandchild_redirect_url.spec())));
+  WaitForLoadStop(web_contents());
+  RenderFrameHost* grandchild_iframe = ChildFrameAt(middle_iframe, 0);
+  ASSERT_TRUE(grandchild_iframe);
+  ASSERT_EQ(grandchild_iframe->GetLastCommittedURL(), grandchild_target_url);
+
+  // The override applies to the redirected navigation request because the
+  // grandchild's origin matches each ancestor's origin (or precursor) both
+  // before and after the redirect on a.test.
+  EXPECT_EQ(grandchild_iframe->GetCookieSettingOverrides().Has(
+                net::CookieSettingOverride::kAllowSameSiteNoneCookiesInSandbox),
+            include_allow_same_site_none_cookies());
+  EXPECT_EQ(EvalJs(grandchild_iframe, "document.body.textContent"),
+            include_allow_same_site_none_cookies() ? "foo=bar" : "");
+  EXPECT_EQ(FetchWithCredentials(grandchild_iframe, grandchild_target_url),
+            include_allow_same_site_none_cookies() ? "foo=bar" : "");
+}
+
 }  // namespace content
diff --git a/content/browser/loader/navigation_url_loader_impl.cc b/content/browser/loader/navigation_url_loader_impl.cc
index 2fab6d58..d004578 100644
--- a/content/browser/loader/navigation_url_loader_impl.cc
+++ b/content/browser/loader/navigation_url_loader_impl.cc
@@ -2164,9 +2164,12 @@
       std::move(device_bound_session_observer),
       std::move(accept_ch_frame_observer));
 
+  allow_same_site_none_cookies_override_ =
+      ShouldAllowSameSiteNoneCookiesInSandbox(*frame_tree_node);
   network_loader_factory_ = CreateNetworkLoaderFactory(
       browser_context_, storage_partition_, frame_tree_node,
-      ukm::SourceIdObj::FromInt64(ukm_source_id_), &bypass_redirect_checks_);
+      ukm::SourceIdObj::FromInt64(ukm_source_id_), &bypass_redirect_checks_,
+      allow_same_site_none_cookies_override_);
 }
 
 // static
@@ -2250,7 +2253,8 @@
     StoragePartitionImpl* storage_partition,
     FrameTreeNode* frame_tree_node,
     const ukm::SourceIdObj& ukm_id,
-    bool* bypass_redirect_checks) {
+    bool* bypass_redirect_checks,
+    bool allow_same_site_none_cookies_override) {
   mojo::PendingRemote<network::mojom::TrustedURLLoaderHeaderClient>
       header_client;
 
@@ -2282,7 +2286,7 @@
       devtools_params.agent_host(), devtools_cookie_overrides);
 
   net::CookieSettingOverrides cookie_overrides;
-  if (ShouldAllowSameSiteNoneCookiesInSandbox(*frame_tree_node)) {
+  if (allow_same_site_none_cookies_override) {
     // Include a CookieSettingOverride in the UrlLoaderFactoryParams for the
     // frame's SharedURLLoaderFactory if the frame contains the
     // `allow-same-site-none-cookies` value in its sandbox policy.
@@ -2363,6 +2367,27 @@
   resource_request_->navigation_redirect_chain.push_back(
       redirect_info_.new_url);
 
+  // The decision to apply the SameSite=None sandbox override depends on the
+  // navigation's tentative origin, which may change after a redirect. Recreate
+  // the network factory and reset the loader if the decision changes so the
+  // override is not applied to the redirected request.
+  if (FrameTreeNode* frame_tree_node =
+          FrameTreeNode::GloballyFindByID(frame_tree_node_id_);
+      frame_tree_node && frame_tree_node->navigation_request()) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/content_security_policy_browsertest.cc b/content/browser/content_security_policy_browsertest.cc
index 76d835f72..fdcb1922 100644
--- a/content/browser/content_security_policy_browsertest.cc
+++ b/content/browser/content_security_policy_browsertest.cc
@@ -11,6 +11,7 @@
 #include "base/memory/raw_ref.h"
 #include "base/notreached.h"
 #include "base/path_service.h"
+#include "base/strings/escape.h"
 #include "base/strings/strcat.h"
 #include "base/strings/stringprintf.h"
 #include "base/test/scoped_feature_list.h"
@@ -447,6 +448,7 @@
 namespace {
 
 constexpr std::string_view kHostA = "a.test";
+constexpr std::string_view kHostSubA = "sub.a.test";
 constexpr std::string_view kHostB = "b.test";
 
 constexpr std::string_view kTopLevelPath = "/top-level.html";
@@ -795,4 +797,94 @@
   EXPECT_EQ(FetchWithCredentials(grandchild_iframe, grandchild_iframe_url), "");
 }
 
+IN_PROC_BROWSER_TEST_P(AllowSameSiteNoneCookiesContentSecurityPolicyBrowserTest,
+                       NestedIframeNavigationCrossOriginRedirect) {
+  GURL top_level = https_server()->GetURL(kHostA, kTopLevelPath);
+  GURL middle_iframe_url = https_server()->GetURL(kHostA, kCrossSiteIframePath);
+  GURL grandchild_target_url =
+      https_server()->GetURL(kHostSubA, kCrossSiteIframePath);
+  GURL grandchild_redirect_url = https_server()->GetURL(
+      kHostA, "/server-redirect?" +
+                  base::EscapeQueryParamValue(grandchild_target_url.spec(),
+                                              /*use_plus=*/false));
+
+  ASSERT_TRUE(SetCookie(web_contents()->GetBrowserContext(),
+                        https_server()->GetURL(kHostSubA, kTopLevelPath),
+                        "foo=bar;SameSite=None;Secure;"));
+
+  // Top a.test embeds a sandboxed a.test iframe which then embeds an inner
+  // iframe that navigates to a.test and is server-redirected to sub.a.test.
+  ASSERT_TRUE(NavigateToURL(shell(), top_level));
+  ASSERT_TRUE(
+      ExecJs(web_contents()->GetPrimaryMainFrame(),
+             JsReplace(R"(document.body.innerHTML =
+                    '<iframe id="middle" src=$1 sandbox=$2></iframe>';)",
+                       middle_iframe_url.spec(), sandbox_iframe_policy())));
+  WaitForLoadStop(web_contents());
+  RenderFrameHost* middle_iframe = ChildFrameAt(shell(), 0);
+
+  ASSERT_TRUE(ExecJs(middle_iframe, JsReplace(R"(document.body.innerHTML =
+                    '<iframe id="grandchild" src=$1></iframe>';)",
+                                              grandchild_redirect_url.spec())));
+  WaitForLoadStop(web_contents());
+  RenderFrameHost* grandchild_iframe = ChildFrameAt(middle_iframe, 0);
+  ASSERT_TRUE(grandchild_iframe);
+  ASSERT_EQ(grandchild_iframe->GetLastCommittedURL(), grandchild_target_url);
+
+  // The override does not apply to the redirected navigation request because
+  // the grandchild's origin no longer matches each ancestor's origin (or
+  // precursor) after the redirect to sub.a.test.
+  EXPECT_FALSE(grandchild_iframe->GetCookieSettingOverrides().Has(
+      net::CookieSettingOverride::kAllowSameSiteNoneCookiesInSandbox));
+  EXPECT_EQ(EvalJs(grandchild_iframe, "document.body.textContent"), "");
+  EXPECT_EQ(FetchWithCredentials(grandchild_iframe, grandchild_target_url), "");
+}
+
+IN_PROC_BROWSER_TEST_P(AllowSameSiteNoneCookiesContentSecurityPolicyBrowserTest,
+                       NestedIframeNavigationSameOriginRedirect) {
+  GURL top_level = https_server()->GetURL(kHostA, kTopLevelPath);
+  GURL middle_iframe_url = https_server()->GetURL(kHostA, kCrossSiteIframePath);
+  GURL grandchild_target_url =
+      https_server()->GetURL(kHostA, kCrossSiteIframePath);
+  GURL grandchild_redirect_url = https_server()->GetURL(
+      kHostA, "/server-redirect?" +
+                  base::EscapeQueryParamValue(grandchild_target_url.spec(),
+                                              /*use_plus=*/false));
+
+  ASSERT_TRUE(SetCookie(web_contents()->GetBrowserContext(),
+                        https_server()->GetURL(kHostA, kTopLevelPath),
+                        "foo=bar;SameSite=None;Secure;"));
+
+  // Top a.test embeds a sandboxed a.test iframe which then embeds an inner
+  // iframe that navigates to a.test and is server-redirected to another
+  // a.test URL.
+  ASSERT_TRUE(NavigateToURL(shell(), top_level));
+  ASSERT_TRUE(
+      ExecJs(web_contents()->GetPrimaryMainFrame(),
+             JsReplace(R"(document.body.innerHTML =
+                    '<iframe id="middle" src=$1 sandbox=$2></iframe>';)",
+                       middle_iframe_url.spec(), sandbox_iframe_policy())));
+  WaitForLoadStop(web_contents());
+  RenderFrameHost* middle_iframe = ChildFrameAt(shell(), 0);
+
+  ASSERT_TRUE(ExecJs(middle_iframe, JsReplace(R"(document.body.innerHTML =
+                    '<iframe id="grandchild" src=$1></iframe>';)",
+                                              grandchild_redirect_url.spec())));
+  WaitForLoadStop(web_contents());
+  RenderFrameHost* grandchild_iframe = ChildFrameAt(middle_iframe, 0);
+  ASSERT_TRUE(grandchild_iframe);
+  ASSERT_EQ(grandchild_iframe->GetLastCommittedURL(), grandchild_target_url);
+
+  // The override applies to the redirected navigation request because the
+  // grandchild's origin matches each ancestor's origin (or precursor) both
+  // before and after the redirect on a.test.
+  EXPECT_EQ(grandchild_iframe->GetCookieSettingOverrides().Has(
+                net::CookieSettingOverride::kAllowSameSiteNoneCookiesInSandbox),
+            include_allow_same_site_none_cookies());
+  EXPECT_EQ(EvalJs(grandchild_iframe, "document.body.textContent"),
+            include_allow_same_site_none_cookies() ? "foo=bar" : "");
+  EXPECT_EQ(FetchWithCredentials(grandchild_iframe, grandchild_target_url),
+            include_allow_same_site_none_cookies() ? "foo=bar" : "");
+}
+
 }  // namespace content
Loading diff…

Original Bug Report

reported by [email protected]

SameSite Cookie Override Bypass via Redirects in Sandboxed Navigation

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: A potential logic vulnerability in NavigationURLLoaderImpl fails to re-evaluate sandbox cookie overrides on redirects. This potentially allows a sandboxed iframe to bypass origin-level cookie restrictions when redirected to same-site, cross-origin targets.

Affected files:

  • content/browser/loader/navigation_url_loader_impl.cc
  • content/browser/renderer_host/render_frame_host_impl.cc
  • components/content_settings/core/common/cookie_settings_base.cc

Estimated timestamp from git blame: 2025-01-23

Summary

A potential security logic vulnerability in the cookie sandbox-override mechanism allows sandboxed iframes to bypass origin-level cookie restrictions when redirected to a same-site, cross-origin destination. This issue stems from NavigationURLLoaderImpl evaluating the cookie override exactly once when the navigation begins, and subsequently reusing the same URLLoaderFactory (and its overrides) across subsequent redirects without recheck.

Root Cause Analysis

There are two potential core logic errors:

  1. Use of Stale State during Start: In content/browser/loader/navigation_url_loader_impl.cc, ShouldAllowSameSiteNoneCookiesInSandbox (lines 487–502) evaluates whether to grant the cookie override kAllowSameSiteNoneCookiesInSandbox based on frame_tree_node.current_frame_host(). This queries the sandbox configuration (active_sandbox_flags()) of the currently committed (old) document rather than the pending/new navigation state.
  2. Persistence across Redirects: When the navigation starts, ShouldAllowSameSiteNoneCookiesInSandbox performs an origin-level check using AncestorsAllowSameSiteNoneCookiesOverride(...) on the initial request URL origin. If this initial destination is same-origin with the frame ancestors, the override is granted and baked into the URLLoaderFactory parameters. During redirects, the same factory is reused. While some overrides (like kStorageAccessGrantEligible) are explicitly removed on cross-origin redirects inside URLLoader::FollowRedirect (defined in services/network/url_loader.cc:1039-1046), kAllowSameSiteNoneCookiesInSandbox is not. However, the Network Service re-verification check (CookieSettingsBase::IsAllowedBySandboxValue inside components/content_settings/core/common/cookie_settings_base.cc:336) only enforces a site-level match (SchemefulSite::IsSameSite) on the redirect target URL against the top-level site. This effectively downgrades the browser’s origin-level ancestor gate to a site-level gate.

Potential Reproduction Steps

(Note: These are potential steps, as our testing tools do not run active execution environments).

  1. Set a SameSite=None; Secure cookie for https://sub.a.example.
  2. Load a top-level page https://a.example/top.html which embeds a sandboxed iframe: <iframe sandbox="allow-scripts allow-same-site-none-cookies" src="https://a.example/middle.html">.
  3. Inside middle.html (which has an opaque origin with precursor a.example), embed an inner iframe pointing to https://a.example/redirect.
  4. https://a.example/redirect responds with an HTTP 302 redirecting to https://sub.a.example/target.
  5. Observe if the request to sub.a.example carries the SameSite=None cookie. If the navigation had gone directly to sub.a.example from the same sandboxed context, the cookie would have been blocked because sub.a.example is cross-origin to the parent’s precursor a.example (failing the origin-level ancestor check).

Suggested Fix

To address this potential issue, consider:

  1. Ensuring that URLLoader::FollowRedirect in services/network/url_loader.cc removes net::CookieSettingOverride::kAllowSameSiteNoneCookiesInSandbox from the request overrides when the redirect is cross-origin (matching the behavior of kStorageAccessGrantEligible).
  2. Modifying ShouldAllowSameSiteNoneCookiesInSandbox to evaluate sandbox constraints against the pending navigation state/target rather than relying on the initial URL and stale frame host state.

Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040


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