0

Add efficient helpers in SchemefulSite for samesite origin comparisons

Adds SchemefulSite::IsSameSite methods which can compute the same site
check efficiently (i.e. with fast early-return paths and no
allocations).

https://html.spec.whatwg.org/multipage/browsers.html#concept-site-same-site

We also upgrade some callers.

Bug: None
Change-Id: Idd969769ff5e54500a25461c87ece193614eaa77
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6321328
Reviewed-by: Ari Chivukula <arichiv@chromium.org>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Reviewed-by: mmenke <mmenke@chromium.org>
Reviewed-by: Christian Biesinger <cbiesinger@chromium.org>
Commit-Queue: Charlie Harrison <csharrison@chromium.org>
Reviewed-by: Chris Fredrickson <cfredric@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1429192}
This commit is contained in:
Charlie Harrison
2025-03-06 14:54:13 -08:00
committed by Chromium LUCI CQ
parent 13a00fedfc
commit e07e86eb2c
13 changed files with 173 additions and 42 deletions

@ -561,8 +561,7 @@ bool CookieSettingsBase::IsAllowedBySandboxValue(
url::Origin origin = url::Origin::Create(url);
url::Origin first_party_origin = url::Origin::Create(first_party_url);
return origin.IsSameOriginWith(first_party_origin) ||
net::SchemefulSite(origin) == net::SchemefulSite(first_party_origin);
return net::SchemefulSite::IsSameSite(origin, first_party_origin);
}
absl::variant<CookieSettingsBase::AllowAllCookies,
@ -834,12 +833,10 @@ bool CookieSettingsBase::IsAllowedByStorageAccessGrant(
net::CookieSettingOverride::kStorageAccessGrantEligibleViaHeader)) {
return false;
}
// The Storage Access API allows access in A(B(A)) case (or similar). Do the
// same-origin check first for performance reasons.
// The Storage Access API allows access in A(B(A)) case (or similar).
const url::Origin origin = url::Origin::Create(url);
const url::Origin first_party_origin = url::Origin::Create(first_party_url);
if (origin.IsSameOriginWith(first_party_origin) ||
net::SchemefulSite(origin) == net::SchemefulSite(first_party_origin)) {
if (net::SchemefulSite::IsSameSite(origin, first_party_origin)) {
return true;
}
if (GetContentSetting(url, first_party_url,

@ -800,8 +800,8 @@ void IndexedDBContextImpl::ShutdownOnIDBSequence(base::TimeTicks start_time) {
if (!delete_bucket && bucket_locator.storage_key.IsThirdPartyContext()) {
delete_bucket = std::ranges::any_of(
origins_to_purge_on_shutdown_, [&](const url::Origin& origin) {
return net::SchemefulSite(origin) ==
bucket_locator.storage_key.top_level_site();
return bucket_locator.storage_key.top_level_site().IsSameSiteWith(
origin);
});
}

@ -1626,8 +1626,8 @@ NavigationRequest::CreateForSynchronousRendererCommit(
blink::mojom::AncestorChainBit ancestor_chain_bit =
blink::mojom::AncestorChainBit::kSameSite;
if (render_frame_host->ComputeSiteForCookies().IsNull() ||
net::SchemefulSite(origin) != top_level_site ||
!top_level_site.opaque() || origin.opaque()) {
!top_level_site.IsSameSiteWith(origin) || !top_level_site.opaque() ||
origin.opaque()) {
ancestor_chain_bit = blink::mojom::AncestorChainBit::kCrossSite;
}
@ -6339,8 +6339,8 @@ void NavigationRequest::CommitNavigation() {
commit_params_->should_have_sticky_user_activation =
!frame_tree_node_->IsMainFrame() &&
old_frame_host->HasStickyUserActivation() &&
net::SchemefulSite(old_frame_host->GetLastCommittedOrigin()) ==
net::SchemefulSite(origin_to_commit);
net::SchemefulSite::IsSameSite(old_frame_host->GetLastCommittedOrigin(),
origin_to_commit);
// Generate a UKM source and track it on NavigationRequest. This will be
// passed down to the blink::Document to be created, if any, and used for UKM

@ -7,6 +7,7 @@
#include "base/check.h"
#include "content/browser/webid/flags.h"
#include "content/browser/webid/webid_utils.h"
#include "net/base/schemeful_site.h"
namespace content {
@ -359,7 +360,7 @@ bool FederatedProviderFetcher::ShouldSkipWellKnownEnforcementForIdp(
}
// Skip if RP and IDP are same-site.
return webid::IsSameSite(
return net::SchemefulSite::IsSameSite(
render_frame_host_->GetLastCommittedOrigin(),
url::Origin::Create(fetch_result.identity_provider_config_url));
}

@ -22,6 +22,7 @@
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/isolation_info.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/schemeful_site.h"
#include "net/base/url_util.h"
#include "net/cookies/site_for_cookies.h"
#include "net/http/http_request_headers.h"
@ -738,7 +739,7 @@ std::pair<GURL, std::optional<ErrorUrlType>> GetErrorUrlAndType(
return std::make_pair(error_url, ErrorUrlType::kSameOrigin);
}
if (!webid::IsSameSite(error_origin, idp_origin)) {
if (!net::SchemefulSite::IsSameSite(error_origin, idp_origin)) {
return std::make_pair(GURL(), ErrorUrlType::kCrossSite);
}

@ -40,10 +40,8 @@ constexpr net::registry_controlled_domains::PrivateRegistryFilter
bool IsSameSiteWithAncestors(const url::Origin& origin,
RenderFrameHost* render_frame_host) {
while (render_frame_host) {
// Many cases are same-origin, so check that first to speed up the cases
// where the check passes, as IsSameSite() is slower.
if (!origin.IsSameOriginWith(render_frame_host->GetLastCommittedOrigin()) &&
!IsSameSite(origin, render_frame_host->GetLastCommittedOrigin())) {
if (!net::SchemefulSite::IsSameSite(
origin, render_frame_host->GetLastCommittedOrigin())) {
return false;
}
render_frame_host = render_frame_host->GetParent();
@ -119,10 +117,6 @@ bool IsEndpointSameOrigin(const GURL& identity_provider_config_url,
.IsSameOriginWith(endpoint_url);
}
bool IsSameSite(const url::Origin& origin1, const url::Origin& origin2) {
return net::SchemefulSite(origin1) == net::SchemefulSite(origin2);
}
bool ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp(
RenderFrameHost& host,
const GURL& identity_provider_config_url,
@ -464,7 +458,7 @@ FedCmRequesterFrameType ComputeRequesterFrameType(const RenderFrameHost& rfh,
if (!rfh.GetParent()) {
return FedCmRequesterFrameType::kMainFrame;
}
return IsSameSite(requester, embedder)
return net::SchemefulSite::IsSameSite(requester, embedder)
? FedCmRequesterFrameType::kSameSiteIframe
: FedCmRequesterFrameType::kCrossSiteIframe;
}

@ -53,10 +53,6 @@ std::optional<std::string> ComputeConsoleMessageForHttpResponseCode(
bool IsEndpointSameOrigin(const GURL& identity_provider_config_url,
const GURL& endpoint_url);
// Returns whether the two origins are considered same-site (same eTLD+1). Also
// ensures that the scheme is the same.
bool IsSameSite(const url::Origin& origin1, const url::Origin& origin2);
// Returns whether FedCM should fail/skip the accounts endpoint request because
// the user is not signed-in to the IdP.
bool ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp(

@ -17,6 +17,54 @@
namespace net {
namespace {
// When `a_is_site` is true, `a` is actually a SchemefulSite internal
// `site_as_origin_`.
bool IsSameSiteInternal(const url::Origin& a,
const url::Origin& b,
bool a_is_site) {
if (a.opaque() || b.opaque()) {
return a == b;
}
if (a.scheme() != b.scheme()) {
return false;
}
// The remaining code largely matches what `SameDomainOrHost()` would do, with
// one exception: we consider equal-but-empty hosts to be same-site.
// Host equality covers two cases:
// 1. Non-network schemes where origins are passed through unchanged.
// 2. Network schemes where equal hosts will have equal sites (and site
// computation is idempotent in cases where `a` is already a site).
if (a.host() == b.host()) {
return true;
}
std::string_view b_site = GetDomainAndRegistryAsStringPiece(
b, net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
// If either `a_site` or `b_site` is empty, their associated SchemefulSites
// will have origins passed through without modification, and the positive
// result would be covered in the host check above.
if (b_site.empty()) {
return false;
}
// Avoid re-calculating the site for `a` if it has already been done.
std::string_view a_site =
a_is_site
? a.host()
: GetDomainAndRegistryAsStringPiece(
a,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
return a_site == b_site;
}
} // namespace
struct SchemefulSite::ObtainASiteResult {
// This is only set if the supplied origin differs from calculated one.
std::optional<url::Origin> origin;
@ -101,6 +149,20 @@ SchemefulSite& SchemefulSite::operator=(const SchemefulSite& other) = default;
SchemefulSite& SchemefulSite::operator=(SchemefulSite&& other) noexcept =
default;
// static
bool SchemefulSite::IsSameSite(const url::Origin& a, const url::Origin& b) {
bool same_site = IsSameSiteInternal(a, b, /*a_is_site=*/false);
DCHECK_EQ(same_site, SchemefulSite(a) == SchemefulSite(b));
return same_site;
}
bool SchemefulSite::IsSameSiteWith(const url::Origin& other) const {
bool same_site =
IsSameSiteInternal(internal_value(), other, /*a_is_site=*/true);
DCHECK_EQ(same_site, *this == SchemefulSite(other));
return same_site;
}
// static
bool SchemefulSite::FromWire(const url::Origin& site_as_origin,
SchemefulSite* out) {

@ -77,6 +77,13 @@ class NET_EXPORT SchemefulSite {
SchemefulSite& operator=(const SchemefulSite& other);
SchemefulSite& operator=(SchemefulSite&& other) noexcept;
// These methods match the spec algorithm
// https://html.spec.whatwg.org/multipage/browsers.html#concept-site-same-site
// in an efficient way without allocating the SchemefulSite directly.
// They exactly match the semantics of SchemefulSite(a) == SchemefulSite(b).
static bool IsSameSite(const url::Origin& a, const url::Origin& b);
bool IsSameSiteWith(const url::Origin& other) const;
// Tries to construct an instance from a (potentially untrusted) value of the
// internal `site_as_origin_` that got received over an RPC.
//

@ -31,11 +31,77 @@ TEST(SchemefulSiteTest, DifferentOriginSameRegisterableDomain) {
}
}
TEST(SchemefulSiteTest, IsSameSite) {
url::Origin opaque;
struct {
url::Origin a;
url::Origin b;
bool same_site;
} kTestCases[] = {
// Different scheme
{url::Origin::Create(GURL("http://foo.com/")),
url::Origin::Create(GURL("https://foo.com/")), false},
// Different eTLD+1
{url::Origin::Create(GURL("http://bar.com/")),
url::Origin::Create(GURL("http://foo.com/")), false},
// Different opaque
{url::Origin(), url::Origin(), false},
// Opaque / non-opaque
{url::Origin(), url::Origin::Create(GURL("http://foo.com/")), false},
// Different file origins
{url::Origin::Create(GURL("file://foo")),
url::Origin::Create(GURL("file://bar")), false},
// Different opaque, one derived from the other.
{opaque, opaque.DeriveNewOpaqueOrigin(), false},
// Same opaque
{opaque, opaque, true},
// Equal hosts
{url::Origin::Create(GURL("http://bar.foo.com/")),
url::Origin::Create(GURL("http://bar.foo.com/")), true},
// Equal and empty hosts
{url::Origin::Create(GURL("file://")),
url::Origin::Create(GURL("file://")), true},
// Equal file origins
{url::Origin::Create(GURL("file://foo")),
url::Origin::Create(GURL("file://foo")), true},
// Equal eTLD+1
{url::Origin::Create(GURL("http://bar.foo.com/")),
url::Origin::Create(GURL("http://baz.foo.com/")), true},
// Equal except port
{url::Origin::Create(GURL("http://bar.foo.com:80/")),
url::Origin::Create(GURL("http://baz.foo.com:81/")), true},
};
for (const auto& test : kTestCases) {
SCOPED_TRACE(::testing::Message() << test.a << " " << test.b);
EXPECT_EQ(test.same_site, SchemefulSite::IsSameSite(test.a, test.b));
EXPECT_EQ(test.same_site, SchemefulSite::IsSameSite(test.b, test.a));
EXPECT_EQ(test.same_site, SchemefulSite(test.a).IsSameSiteWith(test.b));
EXPECT_EQ(test.same_site, SchemefulSite(test.b).IsSameSiteWith(test.a));
EXPECT_TRUE(SchemefulSite::IsSameSite(test.a, test.a));
EXPECT_TRUE(SchemefulSite::IsSameSite(test.b, test.b));
EXPECT_TRUE(SchemefulSite(test.a).IsSameSiteWith(test.a));
EXPECT_TRUE(SchemefulSite(test.b).IsSameSiteWith(test.b));
}
}
TEST(SchemefulSiteTest, Operators) {
// Create a list of origins that should all have different schemeful sites.
// These are in ascending order.
auto kTestOrigins = std::to_array<url::Origin>({
url::Origin::Create(GURL("data:text/html,<body>Hello World</body>")),
url::Origin::Create(GURL("file://")),
url::Origin::Create(GURL("file://foo")),
url::Origin::Create(GURL("http://a.bar.test")),
url::Origin::Create(GURL("http://c.test")),
@ -54,6 +120,9 @@ TEST(SchemefulSiteTest, Operators) {
SCOPED_TRACE(site1.GetDebugString());
EXPECT_EQ(site1, site1);
EXPECT_TRUE(
SchemefulSite::IsSameSite(kTestOrigins[first], kTestOrigins[first]));
EXPECT_TRUE(site1.IsSameSiteWith(kTestOrigins[first]));
EXPECT_FALSE(site1 < site1);
// Check the operators work on copies.
@ -70,6 +139,9 @@ TEST(SchemefulSiteTest, Operators) {
EXPECT_FALSE(site2 < site1);
EXPECT_FALSE(site1 == site2);
EXPECT_FALSE(site2 == site1);
EXPECT_FALSE(
SchemefulSite::IsSameSite(kTestOrigins[first], kTestOrigins[second]));
EXPECT_FALSE(site1.IsSameSiteWith(kTestOrigins[second]));
}
}
}

@ -1151,8 +1151,7 @@ bool ShouldAddInitialStorageAccessApiOverride(
StorageAccessNetRequestKind kind = kCrossSite;
if (request_initiator->IsSameOriginWith(origin)) {
kind = kSameOrigin;
} else if (SchemefulSite(request_initiator.value()) ==
SchemefulSite(origin)) {
} else if (SchemefulSite::IsSameSite(request_initiator.value(), origin)) {
kind = kCrossOriginSameSite;
}
if (emit_metrics) {

@ -196,7 +196,7 @@ std::optional<StorageKey> StorageKey::Deserialize(std::string_view in) {
// Neither should be opaque and they cannot match as that would mean
// we should have simply encoded the origin and the input is malformed.
if (key_origin.opaque() || key_top_level_site.opaque() ||
net::SchemefulSite(key_origin) == key_top_level_site) {
key_top_level_site.IsSameSiteWith(key_origin)) {
return std::nullopt;
}
@ -556,7 +556,7 @@ StorageKey StorageKey::CreateFromOriginAndIsolationInfo(
// CrossSite. Otherwise if the top level site matches the new origin and the
// site for cookies isn't empty it must be SameSite.
if (!origin.opaque() && !top_level_site.opaque() &&
net::SchemefulSite(origin) == top_level_site &&
top_level_site.IsSameSiteWith(origin) &&
!isolation_info.site_for_cookies().IsNull()) {
ancestor_chain_bit = blink::mojom::AncestorChainBit::kSameSite;
}
@ -588,13 +588,13 @@ StorageKey StorageKey::WithOrigin(const url::Origin& origin) const {
// necessarily be kSameSite if the TLS and origin do match, so we won't
// adjust the other way.
if (ancestor_chain_bit == blink::mojom::AncestorChainBit::kSameSite &&
net::SchemefulSite(origin) != top_level_site_) {
!top_level_site_.IsSameSiteWith(origin)) {
ancestor_chain_bit = blink::mojom::AncestorChainBit::kCrossSite;
}
if (ancestor_chain_bit_if_third_party_enabled ==
blink::mojom::AncestorChainBit::kSameSite &&
net::SchemefulSite(origin) != top_level_site_if_third_party_enabled) {
!top_level_site_if_third_party_enabled.IsSameSiteWith(origin)) {
ancestor_chain_bit_if_third_party_enabled =
blink::mojom::AncestorChainBit::kCrossSite;
}
@ -699,7 +699,7 @@ std::string StorageKey::Serialize() const {
.GetTupleOrPrecursorTupleIfOpaque()
.Serialize(),
});
} else if (top_level_site_ == net::SchemefulSite(origin_)) {
} else if (top_level_site_.IsSameSiteWith(origin_)) {
// Case 2.
return base::StrCat({
origin_.GetURL().spec(),
@ -837,9 +837,8 @@ bool StorageKey::MatchesOriginForTrustedStorageDeletion(
// SchemefulSites.
// TODO(crbug.com/1410196): Test that StorageKeys corresponding to anonymous
// iframes are handled appropriately here.
return IsFirstPartyContext()
? (origin_ == origin)
: (top_level_site_ == net::SchemefulSite(origin));
return IsFirstPartyContext() ? (origin_ == origin)
: (top_level_site_.IsSameSiteWith(origin));
}
bool StorageKey::MatchesRegistrableDomainForTrustedStorageDeletion(
@ -877,7 +876,7 @@ bool StorageKey::IsValid() const {
// If this key's "normal" members indicate a 3p key, then the
// *_if_third_party_enabled counterparts must match them.
if (!origin_.opaque() &&
(top_level_site_ != net::SchemefulSite(origin_) ||
(!top_level_site_.IsSameSiteWith(origin_) ||
ancestor_chain_bit_ != blink::mojom::AncestorChainBit::kSameSite)) {
if (top_level_site_ != top_level_site_if_third_party_enabled_) {
return false;
@ -890,13 +889,13 @@ bool StorageKey::IsValid() const {
// If top_level_site* is cross-site to origin, then ancestor_chain_bit* must
// indicate that. An opaque top_level_site* must have a cross-site
// ancestor_chain_bit*.
if (top_level_site_ != net::SchemefulSite(origin_)) {
if (!top_level_site_.IsSameSiteWith(origin_)) {
if (ancestor_chain_bit_ != blink::mojom::AncestorChainBit::kCrossSite) {
return false;
}
}
if (top_level_site_if_third_party_enabled_ != net::SchemefulSite(origin_)) {
if (!top_level_site_if_third_party_enabled_.IsSameSiteWith(origin_)) {
if (ancestor_chain_bit_if_third_party_enabled_ !=
blink::mojom::AncestorChainBit::kCrossSite) {
return false;
@ -908,11 +907,11 @@ bool StorageKey::IsValid() const {
if (nonce_->is_empty()) {
return false;
}
if (top_level_site_ != net::SchemefulSite(origin_)) {
if (!top_level_site_.IsSameSiteWith(origin_)) {
return false;
}
if (top_level_site_if_third_party_enabled_ != net::SchemefulSite(origin_)) {
if (!top_level_site_if_third_party_enabled_.IsSameSiteWith(origin_)) {
return false;
}

@ -229,6 +229,9 @@ class COMPONENT_EXPORT(URL) Origin {
// are exact matches. Two opaque origins are same-origin only if their
// internal nonce values match. A non-opaque origin is never same-origin with
// an opaque origin.
//
// If you are looking for a same _site_ check between origins, see
// net::SchemefulSite::IsSameSite.
bool IsSameOriginWith(const Origin& other) const;
// Non-opaque origin is "same-origin" with `url` if their schemes, hosts, and