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:

committed by
Chromium LUCI CQ

parent
67592416cf
commit
f365e8df5b
components/content_settings/core/common
content/browser/network
net
services/network
third_party/blink/web_tests
VirtualTestSuites
external
wpt
storage-access-api
virtual
saa-top-level-site-cors-exception
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());
|
||||
|
10
third_party/blink/web_tests/VirtualTestSuites
vendored
10
third_party/blink/web_tests/VirtualTestSuites
vendored
@ -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.");
|
||||
|
3
third_party/blink/web_tests/external/wpt/storage-access-api/resources/embedded_responder.js
vendored
3
third_party/blink/web_tests/external/wpt/storage-access-api/resources/embedded_responder.js
vendored
@ -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);
|
||||
|
4
third_party/blink/web_tests/virtual/saa-top-level-site-cors-exception/README.md
vendored
Normal file
4
third_party/blink/web_tests/virtual/saa-top-level-site-cors-exception/README.md
vendored
Normal file
@ -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`
|
5
third_party/blink/web_tests/virtual/saa-top-level-site-cors-exception/external/wpt/storage-access-api/requestStorageAccess-cross-site-sibling-iframes.sub.https.window-expected.txt
vendored
Normal file
5
third_party/blink/web_tests/virtual/saa-top-level-site-cors-exception/external/wpt/storage-access-api/requestStorageAccess-cross-site-sibling-iframes.sub.https.window-expected.txt
vendored
Normal file
@ -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">
|
||||
|
Reference in New Issue
Block a user