CVE-2026-79205
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/loader/navigation_url_loader_impl.cc |
modified |
Files Changed
content/browser/content_security_policy_browsertest.cccontent/browser/loader/navigation_url_loader_impl.cc
Patch
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()) {
Regression Test / PoC
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
Original Bug Report
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.cccontent/browser/renderer_host/render_frame_host_impl.cccomponents/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:
- 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 overridekAllowSameSiteNoneCookiesInSandboxbased onframe_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. - Persistence across Redirects: When the navigation starts,
ShouldAllowSameSiteNoneCookiesInSandboxperforms an origin-level check usingAncestorsAllowSameSiteNoneCookiesOverride(...)on the initial request URL origin. If this initial destination is same-origin with the frame ancestors, the override is granted and baked into theURLLoaderFactoryparameters. During redirects, the same factory is reused. While some overrides (likekStorageAccessGrantEligible) are explicitly removed on cross-origin redirects insideURLLoader::FollowRedirect(defined inservices/network/url_loader.cc:1039-1046),kAllowSameSiteNoneCookiesInSandboxis not. However, the Network Service re-verification check (CookieSettingsBase::IsAllowedBySandboxValueinsidecomponents/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).
- Set a
SameSite=None; Securecookie forhttps://sub.a.example. - Load a top-level page
https://a.example/top.htmlwhich embeds a sandboxed iframe:<iframe sandbox="allow-scripts allow-same-site-none-cookies" src="https://a.example/middle.html">. - Inside
middle.html(which has an opaque origin with precursora.example), embed an inner iframe pointing tohttps://a.example/redirect. https://a.example/redirectresponds with an HTTP302redirecting tohttps://sub.a.example/target.- Observe if the request to
sub.a.examplecarries theSameSite=Nonecookie. If the navigation had gone directly tosub.a.examplefrom the same sandboxed context, the cookie would have been blocked becausesub.a.exampleis cross-origin to the parent’s precursora.example(failing the origin-level ancestor check).
Suggested Fix
To address this potential issue, consider:
- Ensuring that
URLLoader::FollowRedirectinservices/network/url_loader.ccremovesnet::CookieSettingOverride::kAllowSameSiteNoneCookiesInSandboxfrom the request overrides when the redirect is cross-origin (matching the behavior ofkStorageAccessGrantEligible). - Modifying
ShouldAllowSameSiteNoneCookiesInSandboxto 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.