blob: 13e0fce7403d26956aaca8a4c29e8d844b512177 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/renderer_host/cookie_utils.h"
#include <algorithm>
#include <cstddef>
#include <ostream>
#include <string>
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "base/unguessable_token.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/navigation_or_document_handle.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/cookie_access_details.h"
#include "content/public/browser/legacy_tech_cookie_issue_details.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "net/cookies/cookie_constants.h"
#include "net/cookies/cookie_inclusion_status.h"
#include "net/cookies/cookie_setting_override.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom-shared.h"
#include "url/gurl.h"
namespace content {
namespace {
void PotentiallyRecordNonAsciiCookieNameValue(
RenderFrameHost* rfh,
CookieAccessDetails::Type access_type,
const std::string& name,
const std::string& value) {
CHECK(rfh);
if (access_type != CookieAccessDetails::Type::kChange) {
return;
}
// Our data collection policy disallows collecting UKMs while prerendering.
// See //content/browser/preloading/prerender/README.md and ask the team to
// explore options to record data for prerendering pages if we need to
// support the case.
if (rfh->IsInLifecycleState(RenderFrameHost::LifecycleState::kPrerendering)) {
return;
}
bool name_has_non_ascii = !base::IsStringASCII(name);
bool value_has_non_ascii = !base::IsStringASCII(value);
if (name_has_non_ascii || value_has_non_ascii) {
ukm::SourceId source_id = rfh->GetPageUkmSourceId();
auto event = ukm::builders::CookieHasNonAsciiCharacter(source_id);
// The event itself is what we're interested in, the value of "true" here
// can be ignored.
if (name_has_non_ascii) {
event.SetName(true);
}
if (value_has_non_ascii) {
event.SetValue(true);
}
event.Record(ukm::UkmRecorder::Get());
}
}
void PotentiallyRecordCookieOriginMismatch(
RenderFrameHost* rfh,
CookieAccessDetails::Type access_type,
const net::CookieInclusionStatus& status) {
CHECK(rfh);
if (access_type != CookieAccessDetails::Type::kRead) {
return;
}
// Our data collection policy disallows collecting UKMs while prerendering.
// See //content/browser/preloading/prerender/README.md and ask the team to
// explore options to record data for prerendering pages if we need to
// support the case.
if (rfh->IsInLifecycleState(RenderFrameHost::LifecycleState::kPrerendering)) {
return;
}
const bool port_mismatch =
status.HasWarningReason(
net::CookieInclusionStatus::WarningReason::WARN_PORT_MISMATCH) ||
status.HasExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_PORT_MISMATCH);
const bool scheme_mismatch =
status.HasWarningReason(
net::CookieInclusionStatus::WarningReason::WARN_SCHEME_MISMATCH) ||
status.HasExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_SCHEME_MISMATCH);
if (port_mismatch || scheme_mismatch) {
ukm::SourceId source_id = rfh->GetPageUkmSourceId();
auto event = ukm::builders::Cookies_Blocked_DueToOriginMismatch(source_id);
// The event itself is what we're interested in, the value of "true" here
// can be ignored.
if (port_mismatch) {
event.SetPortMismatch(true);
}
if (scheme_mismatch) {
event.SetSchemeMismatch(true);
}
event.Record(ukm::UkmRecorder::Get());
}
}
void RecordCookiesExemptedByTopLevelStorage(ukm::SourceId source_id,
int affected_cookies_in_request) {
ukm::builders::RequestStorageAccessFor_TopLevelStorageIsExemptionReason(
source_id)
.SetNumberOfCookies(ukm::GetExponentialBucketMin(
affected_cookies_in_request, /*bucket_spacing=*/2.0))
.Record(ukm::UkmRecorder::Get());
}
// Relies on checks in RecordPartitionedCookiesUKMs to confirm that that the
// cookie name is not "receive-cookie-deprecation", that cookie is first party
// partitioned and the RenderFrameHost is not prerendering.
void RecordFirstPartyPartitionedCookieCrossSiteContextUKM(
RenderFrameHostImpl* render_frame_host_impl,
const net::CanonicalCookie& cookie,
const ukm::SourceId& source_id) {
// Same-site embed with cross-site ancestors (ABA embeds) have a null site
// for cookies since it is a cross-site context. If the result of
// ComputeSiteForCookies is first-party that means we are not in an ABA
// embedded context.
bool has_cross_site_ancestor =
!render_frame_host_impl->ComputeSiteForCookies().IsFirstParty(
GURL(base::StrCat({url::kHttpsScheme, url::kStandardSchemeSeparator,
cookie.DomainWithoutDot()})));
ukm::builders::Cookies_FirstPartyPartitionedInCrossSiteContextV3(source_id)
.SetCookiePresent(has_cross_site_ancestor)
.Record(ukm::UkmRecorder::Get());
}
// Relies on checks in RecordPartitionedCookiesUKMs to confirm that that the
// cookie is partitioned, the cookie name is not
// "receive-cookie-deprecation" and the RenderFrameHost is not prerendering.
void RecordPartitionedCookieUseV2UKM(RenderFrameHost* rfh,
const net::CanonicalCookie& cookie,
const ukm::SourceId& source_id) {
ukm::builders::PartitionedCookiePresentV2(source_id)
.SetPartitionedCookiePresentV2(true)
.Record(ukm::UkmRecorder::Get());
}
void RecordPartitionedCookiesUKMs(RenderFrameHostImpl* render_frame_host_impl,
const net::CanonicalCookie& cookie) {
// Our data collection policy disallows collecting UKMs while prerendering.
// See //content/browser/preloading/prerender/README.md and ask the team to
// explore options to record data for prerendering pages if we need to
// support the case.
if (render_frame_host_impl->IsInLifecycleState(
RenderFrameHost::LifecycleState::kPrerendering)) {
return;
}
// Cookies_FirstPartyPartitionedInCrossSiteContextV3 and
// PartitionedCookiePresentV2 both measure cookies
// without the name of 'receive-cookie-deprecation'. Return here to ensure
// that the metrics do not include those cookies.
if (cookie.Name() == "receive-cookie-deprecation") {
return;
}
ukm::SourceId source_id = render_frame_host_impl->GetPageUkmSourceId();
if (cookie.IsFirstPartyPartitioned()) {
RecordFirstPartyPartitionedCookieCrossSiteContextUKM(render_frame_host_impl,
cookie, source_id);
}
RecordPartitionedCookieUseV2UKM(render_frame_host_impl, cookie, source_id);
}
void RecordRedirectContextDowngradeUKM(RenderFrameHost* rfh,
CookieAccessDetails::Type access_type,
const net::CanonicalCookie& cookie,
const GURL& url) {
CHECK(rfh);
// Our data collection policy disallows collecting UKMs while prerendering.
// See //content/browser/preloading/prerender/README.md and ask the team to
// explore options to record data for prerendering pages if we need to
// support the case.
if (rfh->IsInLifecycleState(RenderFrameHost::LifecycleState::kPrerendering)) {
return;
}
ukm::SourceId source_id = rfh->GetPageUkmSourceId();
int64_t samesite_value = static_cast<int64_t>(cookie.SameSite());
if (access_type == CookieAccessDetails::Type::kRead) {
base::TimeDelta cookie_age = base::Time::Now() - cookie.CreationDate();
ukm::builders::SamesiteRedirectContextDowngrade(source_id)
.SetSamesiteValueReadPerCookie(samesite_value)
.SetAgePerCookie(
ukm::GetExponentialBucketMinForUserTiming(cookie_age.InMinutes()))
.Record(ukm::UkmRecorder::Get());
} else {
CHECK(access_type == CookieAccessDetails::Type::kChange);
ukm::builders::SamesiteRedirectContextDowngrade(source_id)
.SetSamesiteValueWritePerCookie(samesite_value)
.Record(ukm::UkmRecorder::Get());
}
}
void RecordSchemefulContextDowngradeUKM(
RenderFrameHost* rfh,
CookieAccessDetails::Type access_type,
const net::CookieInclusionStatus& status,
const GURL& url) {
CHECK(rfh);
// Our data collection policy disallows collecting UKMs while prerendering.
// See //content/browser/preloading/prerender/README.md and ask the team to
// explore options to record data for prerendering pages if we need to
// support the case.
if (rfh->IsInLifecycleState(RenderFrameHost::LifecycleState::kPrerendering)) {
return;
}
ukm::SourceId source_id = rfh->GetPageUkmSourceId();
auto downgrade_metric =
static_cast<int64_t>(status.GetBreakingDowngradeMetricsEnumValue(url));
if (access_type == CookieAccessDetails::Type::kRead) {
ukm::builders::SchemefulSameSiteContextDowngrade(source_id)
.SetRequestPerCookie(downgrade_metric)
.Record(ukm::UkmRecorder::Get());
} else {
CHECK(access_type == CookieAccessDetails::Type::kChange);
ukm::builders::SchemefulSameSiteContextDowngrade(source_id)
.SetResponsePerCookie(downgrade_metric)
.Record(ukm::UkmRecorder::Get());
}
}
bool ShouldReportDevToolsIssueForStatus(
const net::CookieInclusionStatus& status) {
return status.ShouldWarn() ||
status.HasExclusionReason(net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_DOMAIN_NON_ASCII) ||
status.HasExclusionReason(net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_PORT_MISMATCH) ||
status.HasExclusionReason(net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_SCHEME_MISMATCH) ||
status.HasExclusionReason(
net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_THIRD_PARTY_BLOCKED_WITHIN_FIRST_PARTY_SET) ||
status.HasExclusionReason(net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_THIRD_PARTY_PHASEOUT) ||
status.exemption_reason() ==
net::CookieInclusionStatus::ExemptionReason::k3PCDMetadata ||
status.exemption_reason() ==
net::CookieInclusionStatus::ExemptionReason::k3PCDHeuristics;
}
bool IsCrossOriginSameSiteNetworkAccessWithStorageAccessEligible(
const network::mojom::CookieAccessDetailsPtr& cookie_details) {
if (!cookie_details->frame_origin ||
!cookie_details->cookie_setting_overrides.Has(
net::CookieSettingOverride::kStorageAccessGrantEligible)) {
// `frame_origin` is unset for script accesses, and network accesses whose
// IsolationInfo's `frame_origin` was nullptr.
return false;
}
const url::Origin origin = url::Origin::Create(cookie_details->url);
return !origin.IsSameOriginWith(cookie_details->frame_origin.value()) &&
net::SchemefulSite::IsSameSite(origin,
cookie_details->frame_origin.value());
}
} // namespace
void SplitCookiesIntoAllowedAndBlocked(
const network::mojom::CookieAccessDetailsPtr& cookie_details,
CookieAccessDetails::Source source,
CookieAccessDetails* allowed,
CookieAccessDetails* blocked) {
// For some cases `site_for_cookies` representative url is empty when
// OnCookieAccess is triggered for a third party. For example iframe third
// party accesses cookies when TPCD Metadata allows third party cookie access.
//
// Make `first_party_url` considering both `top_frame_origin` and
// `site_for_cookies` which is similar with GetFirstPartyURL() in
// components/content_settings/core/common/cookie_settings_base.h.
// If the `top_frame_origin` is non-opaque, it is chosen; otherwise, the
// `site_for_cookies` representative url is used.
const GURL first_party_url =
cookie_details->top_frame_origin.opaque()
? cookie_details->site_for_cookies.RepresentativeUrl()
: cookie_details->top_frame_origin.GetURL();
*allowed = CookieAccessDetails({cookie_details->type,
cookie_details->url,
first_party_url,
{},
/* blocked_by_policy=*/false,
cookie_details->is_ad_tagged,
cookie_details->cookie_setting_overrides,
cookie_details->site_for_cookies,
source});
int allowed_count = std::ranges::count_if(
cookie_details->cookie_list,
[](const network::mojom::CookieOrLineWithAccessResultPtr&
cookie_and_access_result) {
// "Included" cookies have no exclusion reasons so we don't also have to
// check for
// !(net::CookieInclusionStatus::ExclusionReason::EXCLUDE_USER_PREFERENCES).
return cookie_and_access_result->access_result.status.IsInclude();
});
allowed->cookie_access_result_list.reserve(allowed_count);
*blocked = CookieAccessDetails({cookie_details->type,
cookie_details->url,
first_party_url,
{},
/* blocked_by_policy=*/true,
cookie_details->is_ad_tagged,
cookie_details->cookie_setting_overrides,
cookie_details->site_for_cookies,
source});
int blocked_count = std::ranges::count_if(
cookie_details->cookie_list,
[](const network::mojom::CookieOrLineWithAccessResultPtr&
cookie_and_access_result) {
return cookie_and_access_result->access_result.status
.ExcludedByUserPreferencesOrTPCD();
});
blocked->cookie_access_result_list.reserve(blocked_count);
for (const auto& cookie_and_access_result : cookie_details->cookie_list) {
if (cookie_and_access_result->access_result.status
.ExcludedByUserPreferencesOrTPCD()) {
blocked->cookie_access_result_list.emplace_back(
std::move(cookie_and_access_result->cookie_or_line->get_cookie()),
cookie_and_access_result->access_result);
} else if (cookie_and_access_result->access_result.status.IsInclude()) {
allowed->cookie_access_result_list.emplace_back(
std::move(cookie_and_access_result->cookie_or_line->get_cookie()),
cookie_and_access_result->access_result);
}
}
}
// Logs cookie issues to DevTools Issues Panel and logs events to UseCounters
// and UKM for a single cookie-accessed event.
// TODO(crbug.com/40632967): Remove when no longer needed.
void EmitCookieWarningsAndMetrics(
RenderFrameHostImpl* rfh,
const network::mojom::CookieAccessDetailsPtr& cookie_details) {
RenderFrameHostImpl* root_frame_host = rfh->GetMainFrame();
if (!root_frame_host->IsActive())
return;
bool samesite_treated_as_lax_cookies = false;
bool samesite_none_insecure_cookies = false;
bool breaking_context_downgrade = false;
bool lax_allow_unsafe_cookies = false;
bool samesite_cookie_inclusion_changed_by_cross_site_redirect = false;
bool partitioned_cookies_exist = false;
bool cookie_has_not_been_refreshed_in_201_to_300_days = false;
bool cookie_has_not_been_refreshed_in_301_to_350_days = false;
bool cookie_has_not_been_refreshed_in_351_to_400_days = false;
bool cookie_has_domain_non_ascii = false;
int cookies_exempted_by_top_level_storage_access = 0;
const bool cross_origin_same_site_with_storage_access_eligible =
IsCrossOriginSameSiteNetworkAccessWithStorageAccessEligible(
cookie_details);
bool cross_origin_same_site_cookie_via_storage_access_api = false;
for (const network::mojom::CookieOrLineWithAccessResultPtr& cookie :
cookie_details->cookie_list) {
const net::CookieInclusionStatus& status = cookie->access_result.status;
if (ShouldReportDevToolsIssueForStatus(status)) {
std::optional<std::string> devtools_issue_id;
if (status.HasExclusionReason(
net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_THIRD_PARTY_PHASEOUT) ||
status.HasWarningReason(net::CookieInclusionStatus::WarningReason::
WARN_THIRD_PARTY_PHASEOUT)) {
devtools_issue_id = base::UnguessableToken::Create().ToString();
}
devtools_instrumentation::ReportCookieIssue(
root_frame_host, cookie, cookie_details->url,
cookie_details->site_for_cookies,
cookie_details->type == CookieAccessDetails::Type::kRead
? blink::mojom::CookieOperation::kReadCookie
: blink::mojom::CookieOperation::kSetCookie,
cookie_details->devtools_request_id, devtools_issue_id);
}
if (cookie->access_result.status.ShouldWarn()) {
samesite_treated_as_lax_cookies =
samesite_treated_as_lax_cookies ||
status.HasWarningReason(
net::CookieInclusionStatus::WarningReason::
WARN_SAMESITE_UNSPECIFIED_CROSS_SITE_CONTEXT) ||
status.HasWarningReason(
net::CookieInclusionStatus::WarningReason::
WARN_SAMESITE_UNSPECIFIED_LAX_ALLOW_UNSAFE);
samesite_none_insecure_cookies =
samesite_none_insecure_cookies ||
status.HasWarningReason(net::CookieInclusionStatus::WarningReason::
WARN_SAMESITE_NONE_INSECURE);
lax_allow_unsafe_cookies =
lax_allow_unsafe_cookies ||
status.HasWarningReason(
net::CookieInclusionStatus::WarningReason::
WARN_SAMESITE_UNSPECIFIED_LAX_ALLOW_UNSAFE);
samesite_cookie_inclusion_changed_by_cross_site_redirect =
samesite_cookie_inclusion_changed_by_cross_site_redirect ||
status.HasWarningReason(
net::CookieInclusionStatus::WarningReason::
WARN_CROSS_SITE_REDIRECT_DOWNGRADE_CHANGES_INCLUSION);
}
cookie_has_domain_non_ascii =
cookie_has_domain_non_ascii ||
status.HasWarningReason(
net::CookieInclusionStatus::WarningReason::WARN_DOMAIN_NON_ASCII) ||
status.HasExclusionReason(net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_DOMAIN_NON_ASCII);
partitioned_cookies_exist =
partitioned_cookies_exist ||
(cookie->cookie_or_line->is_cookie() &&
cookie->cookie_or_line->get_cookie().IsPartitioned() &&
// Ignore nonced partition keys since this metric is meant to track
// usage of the Partitioned attribute.
!cookie->cookie_or_line->get_cookie().PartitionKey()->nonce());
if (partitioned_cookies_exist) {
RecordPartitionedCookiesUKMs(rfh, cookie->cookie_or_line->get_cookie());
}
if (cookie->access_result.status.exemption_reason() ==
net::CookieInclusionStatus::ExemptionReason::kTopLevelStorageAccess) {
cookies_exempted_by_top_level_storage_access++;
}
breaking_context_downgrade =
breaking_context_downgrade ||
cookie->access_result.status.HasSchemefulDowngradeWarning();
if (cookie->access_result.status.HasSchemefulDowngradeWarning()) {
// Unlike with UMA, do not record cookies that have no schemeful downgrade
// warning.
RecordSchemefulContextDowngradeUKM(rfh, cookie_details->type,
cookie->access_result.status,
cookie_details->url);
}
if (status.HasWarningReason(
net::CookieInclusionStatus::WarningReason::
WARN_CROSS_SITE_REDIRECT_DOWNGRADE_CHANGES_INCLUSION) &&
cookie->cookie_or_line->is_cookie()) {
RecordRedirectContextDowngradeUKM(rfh, cookie_details->type,
cookie->cookie_or_line->get_cookie(),
cookie_details->url);
}
if (cookie->cookie_or_line->is_cookie()) {
PotentiallyRecordNonAsciiCookieNameValue(
rfh, cookie_details->type,
cookie->cookie_or_line->get_cookie().Name(),
cookie->cookie_or_line->get_cookie().Value());
}
PotentiallyRecordCookieOriginMismatch(rfh, cookie_details->type, status);
// In order to anticipate the potential effects of the expiry limit in
// rfc6265bis, we need to check how long it's been since the cookie was
// refreshed (if LastUpdateDate is populated). These three buckets were
// picked so we could engage sites with some granularity around urgency.
// We ignore the space under 200 days as these cookies are not at risk
// of expiring and we ignore the space over 400 days as these cookies
// have already expired. Metrics will take 200 days from M103 to populate.
base::Time last_update_date =
cookie->cookie_or_line->is_cookie()
? cookie->cookie_or_line->get_cookie().LastUpdateDate()
: base::Time();
if (!last_update_date.is_null()) {
int days_since_refresh = (base::Time::Now() - last_update_date).InDays();
cookie_has_not_been_refreshed_in_201_to_300_days |=
days_since_refresh > 200 && days_since_refresh <= 300;
cookie_has_not_been_refreshed_in_301_to_350_days |=
days_since_refresh > 300 && days_since_refresh <= 350;
cookie_has_not_been_refreshed_in_351_to_400_days |=
days_since_refresh > 350 && days_since_refresh <= 400;
}
cross_origin_same_site_cookie_via_storage_access_api |=
cross_origin_same_site_with_storage_access_eligible &&
cookie->access_result.status.IsInclude() &&
cookie->access_result.status.exemption_reason() ==
net::CookieInclusionStatus::ExemptionReason::kStorageAccess;
}
if (samesite_treated_as_lax_cookies) {
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
rfh, blink::mojom::WebFeature::kCookieNoSameSite);
}
if (samesite_none_insecure_cookies) {
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
rfh, blink::mojom::WebFeature::kCookieInsecureAndSameSiteNone);
}
if (breaking_context_downgrade) {
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
rfh, blink::mojom::WebFeature::kSchemefulSameSiteContextDowngrade);
}
if (lax_allow_unsafe_cookies) {
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
rfh, blink::mojom::WebFeature::kLaxAllowingUnsafeCookies);
}
if (samesite_cookie_inclusion_changed_by_cross_site_redirect) {
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
rfh, blink::mojom::WebFeature::
kSameSiteCookieInclusionChangedByCrossSiteRedirect);
}
if (partitioned_cookies_exist) {
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
rfh, blink::mojom::WebFeature::kPartitionedCookies);
}
if (cookie_has_not_been_refreshed_in_201_to_300_days) {
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
rfh,
blink::mojom::WebFeature::kCookieHasNotBeenRefreshedIn201To300Days);
}
if (cookie_has_not_been_refreshed_in_301_to_350_days) {
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
rfh,
blink::mojom::WebFeature::kCookieHasNotBeenRefreshedIn301To350Days);
}
if (cookie_has_not_been_refreshed_in_351_to_400_days) {
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
rfh,
blink::mojom::WebFeature::kCookieHasNotBeenRefreshedIn351To400Days);
}
if (cookie_has_domain_non_ascii) {
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
rfh, blink::mojom::WebFeature::kCookieDomainNonASCII);
}
if (cookies_exempted_by_top_level_storage_access) {
RecordCookiesExemptedByTopLevelStorage(
rfh->GetPageUkmSourceId(),
cookies_exempted_by_top_level_storage_access);
}
if (cross_origin_same_site_cookie_via_storage_access_api) {
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
rfh, blink::mojom::WebFeature::
kCrossOriginSameSiteCookieAccessViaStorageAccessAPI);
}
}
} // namespace content