Medium watch CVSS 4.3 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Network
DescriptionIncorrect authorization in Network
ComponentNetwork
Bug ClassAuthorization Error
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.
Tracker518065628
Fix commit215b9995626a (chromium/src) +127/-4
CWECWE-863
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-30 (per vendor advisory)
Fix landed2026-07-06 (public git)
Disclosed2026-08-25
Patch gap50 days — fix public in source before the advisory

Background

`NavigationURLLoaderImpl`
the browser-process object that drives a navigation’s network load, building the URL loader factory and following server redirects.
`allow-same-site-none-cookies` sandbox override
a cookie-setting override (net::CookieSettingOverride::kAllowSameSiteNoneCookiesInSandbox) that lets a sandboxed frame send SameSite=None cookies when the frame’s origin matches each ancestor’s origin (or opaque precursor).
`ShouldAllowSameSiteNoneCookiesInSandbox`
the predicate that decides whether that override applies, using the navigation’s tentative origin derived from the current request URL and the frame’s ancestor chain.
Server redirect / `FollowRedirect`
when a server responds 3xx, the same navigation loader is reused and FollowRedirect updates the request URL to the redirect target and restarts the load.

Root Cause Analysis

NavigationURLLoaderImpl evaluated ShouldAllowSameSiteNoneCookiesInSandbox a single time in its constructor, using the tentative origin derived from the initial request URL, and baked the resulting override into the network URLLoaderFactory via CreateNetworkLoaderFactory. On a server redirect the existing factory and loader were reused, so the override that was correct for the pre-redirect origin stayed in effect for the post-redirect request even when the new origin (e.g. after a.test → sub.a.test) would no longer pass the exact SchemeHostPort ancestor check. This violated the invariant that the sandbox SameSite=None cookie relaxation applies only when the target origin still matches every ancestor’s origin, letting SameSite=None cookies be attached to a cross-origin redirect target under third-party-cookie blocking.

The fix stores the value in allow_same_site_none_cookies_override_, passes it explicitly into CreateNetworkLoaderFactory, and in FollowRedirect recomputes the predicate for the updated URL; if it changed, it rebuilds the network factory with the new value and clears default_loader_used_ so Restart reissues the redirected request through the corrected factory. This is a no-op for navigations that never had the override.

Key insight
The override decision was computed once from the initial origin and never re-derived after a redirect changed the origin; the fix recomputes it on FollowRedirect and rebuilds the loader factory when the decision flips.

Attack Path

  1. Nest a sandboxed same-origin frame An attacker page on a.test embeds a sandboxed a.test iframe that in turn embeds a grandchild iframe, so the ancestor chain initially satisfies the SameSite=None-in-sandbox origin match.
  2. Point the grandchild at a redirector The grandchild navigates to an a.test URL that server-redirects to a different origin such as sub.a.test, still under the same navigation loader.
  3. Reuse the stale override Because the override was computed only from the initial a.test origin and the factory/loader were reused across the redirect, the redirected request to sub.a.test still carries kAllowSameSiteNoneCookiesInSandbox.
  4. Send cookies that should be blocked The redirected sandboxed context sends SameSite=None cookies to the post-redirect origin even though its origin no longer matches each ancestor, bypassing third-party-cookie blocking for that request.

Impact Assessment

The bug lets a sandboxed, nested frame attach SameSite=None cookies to a redirected cross-origin destination that should have been denied them under third-party-cookie blocking, an incorrect-authorization/cookie-access issue decided in the browser process. Preconditions are a sandboxed iframe using the allow-same-site-none-cookies policy, a nested navigation, and a server redirect that changes the origin away from the ancestor match. The advisory classifies this as incorrect authorization in Network and reports no exploited-in-the-wild status; no memory-safety escalation is involved.

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
  • content/browser/loader/navigation_url_loader_impl.h

Audit Directions

  • Origin-derived decisions cached across redirects
    Audit any security decision (cookie overrides, CORS/CORB state, isolation keys, permissions) computed from the initial request URL/origin and reused when a server redirect changes the origin.
  • Factory/loader reuse on redirect
    Review code that rebuilds vs. reuses URLLoaderFactory/loader objects on FollowRedirect to ensure per-origin parameters are re-derived rather than carried over.
  • SameSite/sandbox ancestor checks
    Look for other call sites of ShouldAllowSameSiteNoneCookiesInSandbox and similar ancestor-origin predicates to confirm they are re-evaluated whenever the effective origin can change mid-navigation.
From 215b9995626a28eb38b01582a4b76cfc9e4f93a8 Mon Sep 17 00:00:00 2001
From: Keita Suzuki <suzukikeita@chromium.org>
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 <suzukikeita@chromium.org>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1657046}
---
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()) {
+    const bool allow_same_site_none_cookies_override =
+        ShouldAllowSameSiteNoneCookiesInSandbox(*frame_tree_node);
+    if (allow_same_site_none_cookies_override !=
+        allow_same_site_none_cookies_override_) {
+      allow_same_site_none_cookies_override_ =
+          allow_same_site_none_cookies_override;
+      network_loader_factory_ = CreateNetworkLoaderFactory(
+          browser_context_, storage_partition_, frame_tree_node,
+          ukm::SourceIdObj::FromInt64(ukm_source_id_), &bypass_redirect_checks_,
+          allow_same_site_none_cookies_override_);
+      default_loader_used_ = false;
+    }
+  }
+
   if (base::FeatureList::IsEnabled(
           network::features::kOffloadAcceptCHFrameCheck)) {
     const url::Origin new_origin = url::Origin::Create(resource_request().url);
diff --git a/content/browser/loader/navigation_url_loader_impl.h b/content/browser/loader/navigation_url_loader_impl.h
index 005ae43..8dcbce48 100644
--- a/content/browser/loader/navigation_url_loader_impl.h
+++ b/content/browser/loader/navigation_url_loader_impl.h
@@ -168,7 +168,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);
 
   void BindAndInterceptNonNetworkURLLoaderFactoryReceiver(
       const GURL& url,
@@ -316,6 +317,11 @@
 
   scoped_refptr<network::SharedURLLoaderFactory> network_loader_factory_;
 
+  // Whether `network_loader_factory_` was created with the cookie-setting
+  // override that allows SameSite=None cookies in sandboxed contexts. Tracked
+  // so the factory can be recreated if the value changes after a redirect.
+  bool allow_same_site_none_cookies_override_ = false;
+
   SubresourceLoaderParams subresource_loader_params_;
 
   std::vector<std::unique_ptr<NavigationLoaderInterceptor>> interceptors_;
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…

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 vm...@google.com

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