|  | // 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 "base/ranges/algorithm.h" | 
|  | #include "content/browser/devtools/devtools_instrumentation.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/common/content_client.h" | 
|  | #include "net/cookies/cookie_inclusion_status.h" | 
|  | #include "services/metrics/public/cpp/metrics_utils.h" | 
|  | #include "services/metrics/public/cpp/ukm_builders.h" | 
|  |  | 
|  | namespace content { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | void RecordRedirectContextDowngradeUKM(RenderFrameHost* rfh, | 
|  | CookieAccessDetails::Type access_type, | 
|  | const net::CanonicalCookie& cookie, | 
|  | const GURL& url) { | 
|  | CHECK(rfh); | 
|  | 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); | 
|  | 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::EXCLUDE_INVALID_SAMEPARTY) || | 
|  | status.HasExclusionReason( | 
|  | net::CookieInclusionStatus::EXCLUDE_DOMAIN_NON_ASCII) || | 
|  | status.HasExclusionReason( | 
|  | net::CookieInclusionStatus:: | 
|  | EXCLUDE_THIRD_PARTY_BLOCKED_WITHIN_FIRST_PARTY_SET); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | void SplitCookiesIntoAllowedAndBlocked( | 
|  | const network::mojom::CookieAccessDetailsPtr& cookie_details, | 
|  | CookieAccessDetails* allowed, | 
|  | CookieAccessDetails* blocked) { | 
|  | *allowed = | 
|  | CookieAccessDetails({cookie_details->type, | 
|  | cookie_details->url, | 
|  | cookie_details->site_for_cookies.RepresentativeUrl(), | 
|  | {}, | 
|  | /* blocked_by_policy=*/false}); | 
|  | int allowed_count = base::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::EXCLUDE_USER_PREFERENCES). | 
|  | return cookie_and_access_result->access_result.status.IsInclude(); | 
|  | }); | 
|  | allowed->cookie_list.reserve(allowed_count); | 
|  |  | 
|  | *blocked = | 
|  | CookieAccessDetails({cookie_details->type, | 
|  | cookie_details->url, | 
|  | cookie_details->site_for_cookies.RepresentativeUrl(), | 
|  | {}, | 
|  | /* blocked_by_policy=*/true}); | 
|  | int blocked_count = base::ranges::count_if( | 
|  | cookie_details->cookie_list, | 
|  | [](const network::mojom::CookieOrLineWithAccessResultPtr& | 
|  | cookie_and_access_result) { | 
|  | return cookie_and_access_result->access_result.status | 
|  | .ExcludedByUserPreferences(); | 
|  | }); | 
|  | blocked->cookie_list.reserve(blocked_count); | 
|  |  | 
|  | for (const auto& cookie_and_access_result : cookie_details->cookie_list) { | 
|  | if (cookie_and_access_result->access_result.status | 
|  | .ExcludedByUserPreferences()) { | 
|  | blocked->cookie_list.emplace_back( | 
|  | std::move(cookie_and_access_result->cookie_or_line->get_cookie())); | 
|  | } else if (cookie_and_access_result->access_result.status.IsInclude()) { | 
|  | allowed->cookie_list.emplace_back( | 
|  | std::move(cookie_and_access_result->cookie_or_line->get_cookie())); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | 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 same_party = false; | 
|  | bool same_party_exclusion_overruled_samesite = false; | 
|  | bool same_party_inclusion_overruled_samesite = 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; | 
|  |  | 
|  | for (const network::mojom::CookieOrLineWithAccessResultPtr& cookie : | 
|  | cookie_details->cookie_list) { | 
|  | const net::CookieInclusionStatus& status = cookie->access_result.status; | 
|  | if (ShouldReportDevToolsIssueForStatus(status)) { | 
|  | 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); | 
|  | } | 
|  |  | 
|  | if (cookie->access_result.status.ShouldWarn()) { | 
|  | samesite_treated_as_lax_cookies = | 
|  | samesite_treated_as_lax_cookies || | 
|  | status.HasWarningReason( | 
|  | net::CookieInclusionStatus:: | 
|  | WARN_SAMESITE_UNSPECIFIED_CROSS_SITE_CONTEXT) || | 
|  | status.HasWarningReason( | 
|  | net::CookieInclusionStatus:: | 
|  | WARN_SAMESITE_UNSPECIFIED_LAX_ALLOW_UNSAFE); | 
|  |  | 
|  | samesite_none_insecure_cookies = | 
|  | samesite_none_insecure_cookies || | 
|  | status.HasWarningReason( | 
|  | net::CookieInclusionStatus::WARN_SAMESITE_NONE_INSECURE); | 
|  |  | 
|  | lax_allow_unsafe_cookies = | 
|  | lax_allow_unsafe_cookies || | 
|  | status.HasWarningReason( | 
|  | net::CookieInclusionStatus:: | 
|  | WARN_SAMESITE_UNSPECIFIED_LAX_ALLOW_UNSAFE); | 
|  |  | 
|  | same_party = same_party || | 
|  | status.HasWarningReason( | 
|  | net::CookieInclusionStatus::WARN_TREATED_AS_SAMEPARTY); | 
|  |  | 
|  | same_party_exclusion_overruled_samesite = | 
|  | same_party_exclusion_overruled_samesite || | 
|  | status.HasWarningReason( | 
|  | net::CookieInclusionStatus:: | 
|  | WARN_SAMEPARTY_EXCLUSION_OVERRULED_SAMESITE); | 
|  |  | 
|  | same_party_inclusion_overruled_samesite = | 
|  | same_party_inclusion_overruled_samesite || | 
|  | status.HasWarningReason( | 
|  | net::CookieInclusionStatus:: | 
|  | WARN_SAMEPARTY_INCLUSION_OVERRULED_SAMESITE); | 
|  |  | 
|  | samesite_cookie_inclusion_changed_by_cross_site_redirect = | 
|  | samesite_cookie_inclusion_changed_by_cross_site_redirect || | 
|  | status.HasWarningReason( | 
|  | net::CookieInclusionStatus:: | 
|  | WARN_CROSS_SITE_REDIRECT_DOWNGRADE_CHANGES_INCLUSION); | 
|  | } | 
|  |  | 
|  | cookie_has_domain_non_ascii = | 
|  | cookie_has_domain_non_ascii || | 
|  | status.HasWarningReason( | 
|  | net::CookieInclusionStatus::WARN_DOMAIN_NON_ASCII) || | 
|  | status.HasExclusionReason( | 
|  | net::CookieInclusionStatus::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()); | 
|  |  | 
|  | 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:: | 
|  | 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); | 
|  | } | 
|  |  | 
|  | // 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; | 
|  | } | 
|  | } | 
|  |  | 
|  | 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 (same_party) { | 
|  | GetContentClient()->browser()->LogWebFeatureForCurrentPage( | 
|  | rfh, blink::mojom::WebFeature::kSamePartyCookieAttribute); | 
|  | } | 
|  |  | 
|  | if (same_party_exclusion_overruled_samesite) { | 
|  | GetContentClient()->browser()->LogWebFeatureForCurrentPage( | 
|  | rfh, | 
|  | blink::mojom::WebFeature::kSamePartyCookieExclusionOverruledSameSite); | 
|  | } | 
|  |  | 
|  | if (same_party_inclusion_overruled_samesite) { | 
|  | GetContentClient()->browser()->LogWebFeatureForCurrentPage( | 
|  | rfh, | 
|  | blink::mojom::WebFeature::kSamePartyCookieInclusionOverruledSameSite); | 
|  | } | 
|  |  | 
|  | 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); | 
|  | } | 
|  | } | 
|  |  | 
|  | }  // namespace content |