0

Add CookieSettingOverride to allow ABA embeds to send cookies using CORS

For now, this functionality is gated behind a base::Feature that is
disabled by default.

This CL does *not* interact with SameSite semantics, and still
maintains that only SameSite=None cookies are allowed in ABA contexts.
This exception is for 3P cookie blocking only.

This exception cannot be applied to cookies accessed via JS.

Bug: 1513690
Change-Id: Id5964224403b7eb9aab69cebe69095530da5baa5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5147868
Reviewed-by: Caitlin Fischer <caitlinfischer@google.com>
Commit-Queue: Dylan Cutler <dylancutler@google.com>
Reviewed-by: Chris Fredrickson <cfredric@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1243468}
This commit is contained in:
Dylan Cutler
2024-01-05 17:22:20 +00:00
committed by Chromium LUCI CQ
parent 67592416cf
commit f365e8df5b
16 changed files with 140 additions and 15 deletions
components/content_settings/core/common
content/browser/network
net
services/network
third_party/blink/web_tests
tools/metrics/histograms/metadata

@ -26,6 +26,20 @@
namespace content_settings {
namespace {
bool IsAllowedByCORS(const net::CookieSettingOverrides& overrides,
const GURL& request_url,
const GURL& first_party_url) {
return overrides.Has(
net::CookieSettingOverride::kCrossSiteCredentialedWithCORS) &&
base::FeatureList::IsEnabled(
net::features::kThirdPartyCookieTopLevelSiteCorsException) &&
net::SchemefulSite(request_url) == net::SchemefulSite(first_party_url);
}
} // namespace
bool CookieSettingsBase::storage_access_api_grants_unpartitioned_storage_ =
false;
@ -365,6 +379,14 @@ CookieSettingsBase::GetCookieSettingInternal(
ACCESS_ALLOWED_3PCD_HEURISTICS_GRANT);
}
if (block_third && IsAllowedByCORS(overrides, request_url, first_party_url)) {
block_third = false;
third_party_cookie_allow_mechanism =
ThirdPartyCookieAllowMechanism::kAllowByCORSException;
FireStorageAccessHistogram(
net::cookie_util::StorageAccessResult::ACCESS_ALLOWED_CORS_EXCEPTION);
}
if (block_third) {
bool has_storage_access_opt_in =
ShouldConsiderStorageAccessGrants(overrides);

@ -98,7 +98,8 @@ class CookieSettingsBase {
kAllowBy3PCDHeuristics = 5,
kAllowByStorageAccess = 6,
kAllowByTopLevelStorageAccess = 7,
kMaxValue = kAllowByTopLevelStorageAccess,
kAllowByCORSException = 8,
kMaxValue = kAllowByCORSException,
};
class CookieSettingWithMetadata {

@ -575,8 +575,12 @@ class ThirdPartyCookiesBlockedHttpCookieBrowserTest
public:
ThirdPartyCookiesBlockedHttpCookieBrowserTest()
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
feature_list_.InitAndEnableFeature(
net::features::kForceThirdPartyCookieBlocking);
feature_list_.InitWithFeatures(
{
net::features::kForceThirdPartyCookieBlocking,
net::features::kThirdPartyCookieTopLevelSiteCorsException,
},
{});
}
~ThirdPartyCookiesBlockedHttpCookieBrowserTest() override = default;
@ -615,19 +619,21 @@ class ThirdPartyCookiesBlockedHttpCookieBrowserTest
return EvalJs(frame, JsReplace(script, url)).ExtractString();
}
std::string FetchWithCredentials(RenderFrameHost* frame, const GURL& url) {
EvalJsResult Fetch(RenderFrameHost* frame,
const GURL& url,
const std::string& mode,
const std::string& credentials) {
constexpr char script[] = R"JS(
fetch($1, { credentials : 'include' }
).then((result) => result.text());
fetch($1, {mode: $2, credentials: $3}).then(result => result.text());
)JS";
return EvalJs(frame, JsReplace(script, url)).ExtractString();
return EvalJs(frame, JsReplace(script, url, mode, credentials));
}
bool CookieStoreEmpty(RenderFrameHost* frame) {
constexpr char script[] = R"JS(
(async () => {
let cookies = await cookieStore.getAll();
return cookies.length == 0 ? true : false;
return cookies.length == 0;
})();
)JS";
return EvalJs(frame, script).ExtractBool();
@ -832,10 +838,51 @@ IN_PROC_BROWSER_TEST_F(
// check if cookies are present on the request.
ASSERT_TRUE(NavigateToURL(web_contents(), EchoCookiesUrl(kHostB)));
EXPECT_TRUE(FetchWithCredentials(
web_contents()->GetPrimaryMainFrame(),
https_server()->GetURL(kHostA, kEchoCookiesWithCorsPath))
.empty());
EXPECT_THAT(Fetch(web_contents()->GetPrimaryMainFrame(),
https_server()->GetURL(kHostA, kEchoCookiesWithCorsPath),
"cors", "include")
.ExtractString(),
net::CookieStringIs(IsEmpty()));
}
IN_PROC_BROWSER_TEST_F(ThirdPartyCookiesBlockedHttpCookieBrowserTest,
TopLevelSiteCorsException) {
// Set and confirm SameSite=None cookie on Site A.
ASSERT_TRUE(SetCookie(
web_contents()->GetBrowserContext(), https_server()->GetURL(kHostA, "/"),
base::StrCat({kSameSiteNoneCookieName, "=1;Secure;SameSite=None;"})));
ASSERT_TRUE(NavigateToURL(web_contents(), EchoCookiesUrl(kHostA)));
// Embed an iframe containing A in B.
ASSERT_EQ(content::ArrangeFramesAndGetContentFromLeaf(
web_contents(), https_server(), base::StrCat({kHostA, "(%s)"}),
{0}, EchoCookiesUrl(kHostB)),
"None");
// Test that a subresource request from B to A can use cookies if it is
// in CORS mode and includes credentials.
EXPECT_EQ(Fetch(ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0),
https_server()->GetURL(kHostA, kEchoCookiesWithCorsPath),
"cors", "include")
.ExtractString(),
base::StrCat({kSameSiteNoneCookieName, "=1"}));
// Test that a subresource request from B to A cannot use cookies if it is
// in CORS mode and omits credentials.
EXPECT_THAT(Fetch(ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0),
https_server()->GetURL(kHostA, kEchoCookiesWithCorsPath),
"cors", "omit")
.ExtractString(),
net::CookieStringIs(IsEmpty()));
// Test that a subresource request from B to A cannot use cookies if it is
// in no-cors mode.
EXPECT_THAT(Fetch(ChildFrameAt(web_contents()->GetPrimaryMainFrame(), 0),
https_server()->GetURL(kHostA, kEchoCookiesWithCorsPath),
"no-cors", "include")
.ExtractString(),
net::CookieStringIs(IsEmpty()));
}
INSTANTIATE_TEST_SUITE_P(/* no label */,

@ -465,6 +465,10 @@ BASE_FEATURE(kForceThirdPartyCookieBlocking,
"ForceThirdPartyCookieBlockingEnabled",
base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE(kThirdPartyCookieTopLevelSiteCorsException,
"ThirdPartyCookieTopLevelSiteCorsException",
base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE(kEnableEarlyHintsOnHttp11,
"EnableEarlyHintsOnHttp11",
base::FEATURE_DISABLED_BY_DEFAULT);

@ -467,6 +467,11 @@ NET_EXPORT BASE_DECLARE_FEATURE(kTimeLimitedInsecureCookies);
// Enables enabling third-party cookie blocking from the command line.
NET_EXPORT BASE_DECLARE_FEATURE(kForceThirdPartyCookieBlocking);
// Enables an exception for third-party cookie blocking when the request is
// same-site with the top-level document, opted into CORS, but embedded in a
// cross-site context.
NET_EXPORT BASE_DECLARE_FEATURE(kThirdPartyCookieTopLevelSiteCorsException);
// Enables Early Hints on HTTP/1.1.
NET_EXPORT BASE_DECLARE_FEATURE(kEnableEarlyHintsOnHttp11);

@ -29,8 +29,13 @@ enum class CookieSettingOverride {
// backs 3PC accesses granted via 3PC deprecation trial.
kSkipTPCDSupport = 3,
kSkipTPCDMetadataGrant = 4,
// Corresponds to checks that may grant 3PCs when a request opts into
// credentials and CORS protection.
// One example are subresource requests that are same-site with the top-level
// site but originate from a cross-site embed.
kCrossSiteCredentialedWithCORS = 5,
kMaxValue = kSkipTPCDMetadataGrant,
kMaxValue = kCrossSiteCredentialedWithCORS,
};
using CookieSettingOverrides = base::EnumSet<CookieSettingOverride,

@ -49,7 +49,8 @@ enum class StorageAccessResult {
ACCESS_ALLOWED_3PCD = 5,
ACCESS_ALLOWED_3PCD_METADATA_GRANT = 6,
ACCESS_ALLOWED_3PCD_HEURISTICS_GRANT = 7,
kMaxValue = ACCESS_ALLOWED_3PCD_HEURISTICS_GRANT,
ACCESS_ALLOWED_CORS_EXCEPTION = 8,
kMaxValue = ACCESS_ALLOWED_CORS_EXCEPTION,
};
// This enum must match the numbering for BreakageIndicatorType in
// histograms/enums.xml. Do not reorder or remove items, only add new items

@ -739,6 +739,12 @@ URLLoader::URLLoader(
url_request_->cookie_setting_overrides().Put(
net::CookieSettingOverride::kTopLevelStorageAccessGrantEligible);
}
if (network::cors::IsCorsEnabledRequestMode(request_mode_) &&
url_request_->site_for_cookies().IsNull() &&
url_request_->allow_credentials()) {
url_request_->cookie_setting_overrides().Put(
net::CookieSettingOverride::kCrossSiteCredentialedWithCORS);
}
AddAdsHeuristicCookieSettingOverrides(
request.is_ad_tagged, url_request_->cookie_setting_overrides());

@ -2449,6 +2449,16 @@
"expires": "Jul 1, 2024",
"owners": ["arichiv@chromium.org"]
},
{
"prefix": "saa-top-level-site-cors-exception",
"platforms": ["Linux", "Mac", "Win"],
"bases": [
"external/wpt/storage-access-api/requestStorageAccess-cross-site-sibling-iframes.sub.https.window.js"
],
"args": ["--enable-features=ThirdPartyCookieTopLevelSiteCorsException"],
"expires": "Jul 1, 2024",
"owners": ["dylancutler@google.com"]
},
{
"prefix": "permission-element",
"platforms": ["Linux", "Mac", "Win"],

@ -264,6 +264,14 @@ function FetchFromFrame(frame, url) {
{ command: "cors fetch", url }, frame.contentWindow);
}
// Makes a subresource request to the provided host in the given frame with
// the mode set to 'no-cors'
function NoCorsSubresourceCookiesFromFrame(frame, host) {
const url = `${host}/storage-access-api/resources/echo-cookie-header.py`;
return PostMessageAndAwaitReply(
{ command: "no-cors fetch", url }, frame.contentWindow);
}
// Tries to set storage access policy, ignoring any errors.
//
// Note: to discourage the writing of tests that assume unpartitioned cookie
@ -295,4 +303,4 @@ function MessageWorker(frame, message = {}) {
function ReadCookiesFromWebSocketConnection(frame, origin) {
return PostMessageAndAwaitReply(
{ command: "get_cookie_via_websocket", origin}, frame.contentWindow);
}
}

@ -75,6 +75,8 @@
assert_true(cookieStringHasCookie("foo", "bar", await FetchSubresourceCookiesFromFrame(crossSiteFrame, wwwAlt)),"crossSiteFrame making same-origin subresource request can access cookies.");
assert_false(cookieStringHasCookie("foo", "bar", await FetchSubresourceCookiesFromFrame(crossOriginFrame, wwwAlt)), "crossOriginFrame making cross-site subresource request to sibling iframe's host should not include cookies.");
assert_false(cookieStringHasCookie("foo", "bar", await NoCorsSubresourceCookiesFromFrame(crossOriginFrame, www)), "crossSiteFrame making no-cors cross-site subresource request to sibling iframe's host should not include cookies.");
assert_false(cookieStringHasCookie("cookie", "monster", await FetchSubresourceCookiesFromFrame(crossSiteFrame, www)),"crossSiteFrame making cross-site subresource request to sibling iframe's host should not include cookies.");
}, "Cross-site sibling iframes should not be able to take advantage of the existing permission grant requested by others.");

@ -75,6 +75,9 @@ window.addEventListener("message", async (event) => {
case "cors fetch":
reply(await fetch(event.data.url, {mode: 'cors', credentials: 'include'}).then((resp) => resp.text()));
break;
case "no-cors fetch":
reply(await fetch(event.data.url, {mode: 'no-cors', credentials: 'include'}).then((resp) => resp.text()));
break;
case "start_dedicated_worker":
worker = new Worker("embedded_worker.js");
reply(undefined);

@ -0,0 +1,4 @@
This directory verifies behavior of the Storage Access API when we grant an
exception to third-party cookie blocking to requests that are to the same
site as the top-level domain, even if they have a cross-site ancestor.
`--enable-features=ThirdPartyCookieTopLevelSiteCorsException`

@ -0,0 +1,5 @@
This is a testharness.js-based test.
[FAIL] Cross-site sibling iframes should not be able to take advantage of the existing permission grant requested by others.
assert_false: crossSiteFrame making cross-site subresource request to sibling iframe's host should not include cookies. expected false got true
Harness: the test ran to completion.

@ -305,6 +305,7 @@ chromium-metrics-reviews@google.com.
<int value="5" label="Allow by 3PCD heuristics"/>
<int value="6" label="Allow by Storage access API"/>
<int value="7" label="Allow by top-level Storage access API"/>
<int value="8" label="Allow by opting into CORS protections"/>
</enum>
</enums>

@ -301,6 +301,7 @@ chromium-metrics-reviews@google.com.
<int value="6"
label="Storage access allowed by 3PCD metadata grants content settings"/>
<int value="7" label="Temporary storage access allowed by 3PCD heuristics"/>
<int value="8" label="Storage access allowed due to CORS opt in"/>
</enum>
<enum name="StorageBucketDurabilityParameter">