blob: d51bb27abfc4880b08941c148f7f1aa46a6e33bb [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Portions of this code based on Mozilla:
// (netwerk/cookie/src/nsCookieService.cpp)
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is mozilla.org code.
*
* The Initial Developer of the Original Code is
* Netscape Communications Corporation.
* Portions created by the Initial Developer are Copyright (C) 2003
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Daniel Witte (dwitte@stanford.edu)
* Michiel van Leeuwen (mvl@exedo.nl)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
#include "net/cookies/canonical_cookie.h"
#include <limits>
#include <optional>
#include <string_view>
#include <tuple>
#include <utility>
#include "base/compiler_specific.h"
#include "base/containers/contains.h"
#include "base/dcheck_is_on.h"
#include "base/feature_list.h"
#include "base/format_macros.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "net/base/features.h"
#include "net/base/url_util.h"
#include "net/cookies/cookie_access_params.h"
#include "net/cookies/cookie_constants.h"
#include "net/cookies/cookie_inclusion_status.h"
#include "net/cookies/cookie_options.h"
#include "net/cookies/cookie_util.h"
#include "net/cookies/parsed_cookie.h"
#include "net/http/http_util.h"
#include "url/gurl.h"
#include "url/url_canon.h"
#include "url/url_util.h"
using base::Time;
namespace net {
namespace {
static constexpr int kMinutesInTwelveHours = 12 * 60;
static constexpr int kMinutesInTwentyFourHours = 24 * 60;
void AppendCookieLineEntry(const CanonicalCookie& cookie,
std::string* cookie_line) {
if (!cookie_line->empty())
*cookie_line += "; ";
// In Mozilla, if you set a cookie like "AAA", it will have an empty token
// and a value of "AAA". When it sends the cookie back, it will send "AAA",
// so we need to avoid sending "=AAA" for a blank token value.
if (!cookie.Name().empty())
*cookie_line += cookie.Name() + "=";
*cookie_line += cookie.Value();
}
// Converts CookieSameSite to CookieSameSiteForMetrics by adding 1 to it.
CookieSameSiteForMetrics CookieSameSiteToCookieSameSiteForMetrics(
CookieSameSite enum_in) {
return static_cast<CookieSameSiteForMetrics>((static_cast<int>(enum_in) + 1));
}
} // namespace
CookieAccessParams::CookieAccessParams(CookieAccessSemantics access_semantics,
CookieScopeSemantics scope_semantics,
bool delegate_treats_url_as_trustworthy)
: access_semantics(access_semantics),
scope_semantics(scope_semantics),
delegate_treats_url_as_trustworthy(delegate_treats_url_as_trustworthy) {}
CanonicalCookie::CanonicalizationResult::CanonicalizationResult(
base::PassKey<CanonicalCookie>,
std::optional<CanonicalizationFailure> failure)
: failure_(failure) {}
std::ostream& operator<<(
std::ostream& os,
const CanonicalCookie::CanonicalizationResult& result) {
if (result) {
os << "(ok)";
} else {
os << result.failure_.value();
}
return os;
}
CanonicalCookie::CanonicalCookie() = default;
CanonicalCookie::CanonicalCookie(const CanonicalCookie& other) = default;
CanonicalCookie::CanonicalCookie(CanonicalCookie&& other) = default;
CanonicalCookie& CanonicalCookie::operator=(const CanonicalCookie& other) =
default;
CanonicalCookie& CanonicalCookie::operator=(CanonicalCookie&& other) = default;
CanonicalCookie::CanonicalCookie(
base::PassKey<CanonicalCookie> pass_key,
std::string name,
std::string value,
std::string domain,
std::string path,
base::Time creation,
base::Time expiration,
base::Time last_access,
base::Time last_update,
bool secure,
bool httponly,
CookieSameSite same_site,
CookiePriority priority,
std::optional<CookiePartitionKey> partition_key,
CookieSourceScheme source_scheme,
int source_port,
CookieSourceType source_type)
: CookieBase(std::move(name),
std::move(domain),
std::move(path),
creation,
secure,
httponly,
same_site,
std::move(partition_key),
source_scheme,
source_port),
value_(std::move(value)),
expiry_date_(expiration),
last_access_date_(last_access),
last_update_date_(last_update),
priority_(priority),
source_type_(source_type) {}
CanonicalCookie::~CanonicalCookie() = default;
// static
Time CanonicalCookie::ParseExpiration(const ParsedCookie& pc,
Time current,
Time server_time) {
// First, try the Max-Age attribute.
if (pc.MaxAge().has_value()) {
int64_t max_age = 0;
// Use the output if StringToInt64 returns true ("perfect" conversion). This
// case excludes overflow/underflow, leading/trailing whitespace, non-number
// strings, and empty string. (ParsedCookie trims whitespace.)
if (base::StringToInt64(pc.MaxAge().value(), &max_age)) {
// RFC 6265bis algorithm for parsing Max-Age:
// "If delta-seconds is less than or equal to zero (0), let expiry-
// time be the earliest representable date and time. ... "
if (max_age <= 0)
return Time::Min();
// "... Otherwise, let the expiry-time be the current date and time plus
// delta-seconds seconds."
return current + base::Seconds(max_age);
} else {
// If the conversion wasn't perfect, but the best-effort conversion
// resulted in an overflow/underflow, use the min/max representable time.
// (This is alluded to in the spec, which says the user agent MAY clip an
// Expires attribute to a saturated time. We'll do the same for Max-Age.)
if (max_age == std::numeric_limits<int64_t>::min())
return Time::Min();
if (max_age == std::numeric_limits<int64_t>::max())
return Time::Max();
}
}
if (!pc.Expires().has_value() || pc.Expires().value().empty()) {
// No expiration.
return Time();
}
// Adjust for clock skew between server and host.
Time parsed_expiry =
cookie_util::ParseCookieExpirationTime(pc.Expires().value());
if (parsed_expiry.is_null()) {
// Invalid expiration.
return Time();
}
Time adjusted_expiry = parsed_expiry + (current - server_time);
static base::MetricsSubSampler metrics_subsampler;
if (metrics_subsampler.ShouldSample(kHistogramSampleProbability)) {
// Record metrics related to prevalence of clock skew.
base::TimeDelta clock_skew = (current - server_time);
// Record the magnitude (absolute value) of the skew in minutes.
int clock_skew_magnitude = clock_skew.magnitude().InMinutes();
// Determine the new expiry with clock skew factored in.
if (clock_skew.is_positive() || clock_skew.is_zero()) {
base::UmaHistogramCustomCounts("Cookie.ClockSkew.AddMinutes.Subsampled",
clock_skew_magnitude, 1,
kMinutesInTwelveHours, 100);
base::UmaHistogramCustomCounts(
"Cookie.ClockSkew.AddMinutes12To24Hours.Subsampled",
clock_skew_magnitude, kMinutesInTwelveHours,
kMinutesInTwentyFourHours, 100);
// Also record the range of minutes added that allowed the cookie to
// avoid expiring immediately.
if (parsed_expiry <= Time::Now() && adjusted_expiry > Time::Now()) {
base::UmaHistogramCustomCounts(
"Cookie.ClockSkew.WithoutAddMinutesExpires.Subsampled",
clock_skew_magnitude, 1, kMinutesInTwentyFourHours, 100);
}
} else if (clock_skew.is_negative()) {
// These histograms only support positive numbers, so negative skews
// will be converted to positive (via magnitude) before recording.
base::UmaHistogramCustomCounts(
"Cookie.ClockSkew.SubtractMinutes.Subsampled", clock_skew_magnitude,
1, kMinutesInTwelveHours, 100);
base::UmaHistogramCustomCounts(
"Cookie.ClockSkew.SubtractMinutes12To24Hours.Subsampled",
clock_skew_magnitude, kMinutesInTwelveHours,
kMinutesInTwentyFourHours, 100);
}
// Record if we were going to expire the cookie before we added the clock
// skew.
base::UmaHistogramBoolean(
"Cookie.ClockSkew.ExpiredWithoutSkew.Subsampled",
parsed_expiry <= Time::Now() && adjusted_expiry > Time::Now());
}
return adjusted_expiry;
}
// static
base::Time CanonicalCookie::ValidateAndAdjustExpiryDate(
base::Time expiry_date,
base::Time creation_date,
net::CookieSourceScheme scheme) {
if (expiry_date.is_null())
return expiry_date;
base::Time fixed_creation_date = creation_date;
if (fixed_creation_date.is_null()) {
// TODO(crbug.com/40800807): Push this logic into
// CanonicalCookie::CreateSanitizedCookie. The four sites that call it
// with a null `creation_date` (CanonicalCookie::Create cannot be called
// this way) are:
// * GaiaCookieManagerService::ForceOnCookieChangeProcessing
// * CookiesSetFunction::Run
// * cookie_store.cc::ToCanonicalCookie
// * network_handler.cc::MakeCookieFromProtocolValues
fixed_creation_date = base::Time::Now();
}
base::Time maximum_expiry_date;
if (!cookie_util::IsTimeLimitedInsecureCookiesEnabled() ||
scheme == net::CookieSourceScheme::kSecure) {
maximum_expiry_date = fixed_creation_date + base::Days(400);
} else {
maximum_expiry_date = fixed_creation_date + base::Hours(3);
}
if (expiry_date > maximum_expiry_date) {
return maximum_expiry_date;
}
return expiry_date;
}
// static
std::unique_ptr<CanonicalCookie> CanonicalCookie::Create(
const GURL& url,
std::string_view cookie_line,
base::Time creation_time,
std::optional<base::Time> server_time,
std::optional<CookiePartitionKey> cookie_partition_key,
CookieSourceType source_type,
CookieInclusionStatus* status) {
// Put a pointer on the stack so the rest of the function can assign to it if
// the default nullptr is passed in.
CookieInclusionStatus blank_status;
if (status == nullptr) {
status = &blank_status;
}
*status = CookieInclusionStatus();
// Check the URL; it may be nonsense since some platform APIs may permit
// it to be specified directly.
if (!url.is_valid()) {
status->AddExclusionReason(
CookieInclusionStatus::ExclusionReason::EXCLUDE_FAILURE_TO_STORE);
return nullptr;
}
ParsedCookie parsed_cookie(cookie_line, status);
static base::MetricsSubSampler metrics_subsampler;
bool collect_metrics =
metrics_subsampler.ShouldSample(kHistogramSampleProbability);
if (collect_metrics) {
// We record this metric before checking validity because the presence of an
// HTAB will invalidate the ParsedCookie.
base::UmaHistogramBoolean("Cookie.NameOrValueHtab.Subsampled",
parsed_cookie.HasInternalHtab());
}
if (!parsed_cookie.IsValid()) {
DVLOG(net::cookie_util::kVlogSetCookies)
<< "WARNING: Couldn't parse cookie";
DCHECK(!status->IsInclude());
// Don't continue, because an invalid ParsedCookie doesn't have any
// attributes.
// TODO(chlily): Log metrics.
return nullptr;
}
if (collect_metrics) {
// Record warning for non-ASCII octecs in the Domain attribute.
// This should lead to rejection of the cookie in the future.
base::UmaHistogramBoolean(
"Cookie.DomainHasNonASCII.Subsampled",
parsed_cookie.Domain() &&
!base::IsStringASCII(parsed_cookie.Domain().value()));
}
std::optional<std::string> cookie_domain =
cookie_util::GetCookieDomainWithString(
url, parsed_cookie.Domain().value_or(""), *status);
if (!cookie_domain) {
DVLOG(net::cookie_util::kVlogSetCookies)
<< "Create() failed to get a valid cookie domain";
status->AddExclusionReason(
CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_DOMAIN);
}
std::string cookie_path = cookie_util::CanonPathWithString(
url, parsed_cookie.Path().value_or(std::string_view()));
Time cookie_server_time(creation_time);
if (server_time.has_value() && !server_time->is_null())
cookie_server_time = server_time.value();
DCHECK(!creation_time.is_null());
CookiePrefix prefix = cookie_util::GetCookiePrefix(parsed_cookie.Name());
bool is_cookie_prefix_valid =
cookie_util::IsCookiePrefixValid(prefix, url, parsed_cookie);
if (collect_metrics) {
base::UmaHistogramEnumeration("Cookie.CookiePrefix.Subsampled", prefix,
COOKIE_PREFIX_LAST);
}
if (parsed_cookie.Name() == "") {
is_cookie_prefix_valid = !HasHiddenPrefixName(parsed_cookie.Value());
}
if (!is_cookie_prefix_valid) {
DVLOG(net::cookie_util::kVlogSetCookies)
<< "Create() failed because the cookie violated prefix rules.";
status->AddExclusionReason(
CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX);
}
bool partition_has_nonce = CookiePartitionKey::HasNonce(cookie_partition_key);
bool is_partitioned_valid = cookie_util::IsCookiePartitionedValid(
url, parsed_cookie, partition_has_nonce);
if (!is_partitioned_valid) {
status->AddExclusionReason(
CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PARTITIONED);
}
// Collect metrics on whether usage of the Partitioned attribute is correct.
// Do not include implicit nonce-based partitioned cookies in these metrics.
if (parsed_cookie.IsPartitioned()) {
if (!partition_has_nonce && collect_metrics) {
base::UmaHistogramBoolean("Cookie.IsPartitionedValid",
is_partitioned_valid);
}
} else if (!partition_has_nonce) {
cookie_partition_key = std::nullopt;
}
if (!status->IsInclude())
return nullptr;
auto [samesite, samesite_string] = parsed_cookie.SameSite();
// The next two sections set the source_scheme_ and source_port_. Normally
// these are taken directly from the url's scheme and port but if the url
// setting this cookie is considered a trustworthy origin then we may make
// some modifications. Note that here we assume that a trustworthy url must
// have a non-secure scheme (http). Since we can't know at this point if a url
// is trustworthy or not, we'll assume it is if the cookie is set with the
// `Secure` attribute.
//
// For both convenience and to try to match expectations, cookies that have
// the `Secure` attribute are modified to look like they were created by a
// secure url. This is helpful because this cookie can be treated like any
// other secure cookie when we're retrieving them and helps to prevent the
// cookie from getting "trapped" if the url loses trustworthiness.
CookieSourceScheme source_scheme;
if (parsed_cookie.IsSecure() || url.SchemeIsCryptographic()) {
// It's possible that a trustworthy origin is setting this cookie with the
// `Secure` attribute even if the url's scheme isn't secure. In that case
// we'll act like it was a secure scheme. This cookie will be rejected later
// if the url isn't allowed to access secure cookies so this isn't a
// problem.
source_scheme = CookieSourceScheme::kSecure;
if (!url.SchemeIsCryptographic()) {
status->AddWarningReason(
CookieInclusionStatus::WarningReason::
WARN_TENTATIVELY_ALLOWING_SECURE_SOURCE_SCHEME);
}
} else {
source_scheme = CookieSourceScheme::kNonSecure;
}
// Get the port, this will get a default value if a port isn't explicitly
// provided. Similar to the source scheme, it's possible that a trustworthy
// origin is setting this cookie with the `Secure` attribute even if the url's
// scheme isn't secure. This function will return 443 to pretend like this
// cookie was set by a secure scheme.
int source_port = CanonicalCookie::GetAndAdjustPortForTrustworthyUrls(
url, parsed_cookie.IsSecure());
Time cookie_expires = CanonicalCookie::ParseExpiration(
parsed_cookie, creation_time, cookie_server_time);
cookie_expires =
ValidateAndAdjustExpiryDate(cookie_expires, creation_time, source_scheme);
auto cc = std::make_unique<CanonicalCookie>(
base::PassKey<CanonicalCookie>(), parsed_cookie.Name(),
parsed_cookie.Value(), std::move(cookie_domain).value_or(std::string()),
std::move(cookie_path), creation_time, cookie_expires, creation_time,
/*last_update=*/base::Time::Now(), parsed_cookie.IsSecure(),
parsed_cookie.IsHttpOnly(), samesite, parsed_cookie.Priority(),
cookie_partition_key, source_scheme, source_port, source_type);
// Check if name or value contains any non-ascii values, exclude if they do.
if (base::FeatureList::IsEnabled(features::kDisallowNonAsciiCookies)) {
if (!base::IsStringASCII(cc->Name()) || !base::IsStringASCII(cc->Value())) {
status->AddExclusionReason(
CookieInclusionStatus::ExclusionReason::EXCLUDE_DISALLOWED_CHARACTER);
}
}
// TODO(chlily): Log metrics.
if (!cc->IsCanonical()) {
status->AddExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_FAILURE_TO_STORE);
return nullptr;
}
RecordCookieSameSiteAttributeValueHistogram(samesite_string);
if (collect_metrics) {
// These metrics capture whether or not a cookie has a Non-ASCII character
// in it, except if kDisallowNonAsciiCookies is enabled.
base::UmaHistogramBoolean("Cookie.HasNonASCII.Name.Subsampled",
!base::IsStringASCII(cc->Name()));
base::UmaHistogramBoolean("Cookie.HasNonASCII.Value.Subsampled",
!base::IsStringASCII(cc->Value()));
// Check for "__" prefixed names, excluding the cookie prefixes.
bool name_prefixed_with_underscores =
(prefix == COOKIE_PREFIX_NONE) &&
parsed_cookie.Name().starts_with("__");
base::UmaHistogramBoolean("Cookie.DoubleUnderscorePrefixedName.Subsampled",
name_prefixed_with_underscores);
}
return cc;
}
// static
std::unique_ptr<CanonicalCookie> CanonicalCookie::CreateSanitizedCookie(
const GURL& url,
const std::string& name,
const std::string& value,
const std::string& domain,
const std::string& path,
base::Time creation_time,
base::Time expiration_time,
base::Time last_access_time,
bool secure,
bool http_only,
CookieSameSite same_site,
CookiePriority priority,
std::optional<CookiePartitionKey> partition_key,
CookieInclusionStatus* status) {
// Put a pointer on the stack so the rest of the function can assign to it if
// the default nullptr is passed in.
CookieInclusionStatus blank_status;
if (status == nullptr) {
status = &blank_status;
}
*status = CookieInclusionStatus();
// Validate consistency of passed arguments.
if (ParsedCookie::ParseTokenString(name) != name) {
status->AddExclusionReason(net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_DISALLOWED_CHARACTER);
} else if (ParsedCookie::ParseValueString(value) != value) {
status->AddExclusionReason(net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_DISALLOWED_CHARACTER);
} else if (ParsedCookie::ParseValueString(path) != path) {
// NOTE: If `path` contains "terminating characters" ('\r', '\n', and
// '\0'), ';', or leading / trailing whitespace, path will be rejected,
// but any other control characters will just get URL-encoded below.
status->AddExclusionReason(net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_DISALLOWED_CHARACTER);
}
// Check if name or value contains any non-ascii values, exclude if they do.
if (base::FeatureList::IsEnabled(features::kDisallowNonAsciiCookies)) {
if (!base::IsStringASCII(name) || !base::IsStringASCII(value)) {
status->AddExclusionReason(
CookieInclusionStatus::ExclusionReason::EXCLUDE_DISALLOWED_CHARACTER);
}
}
// Validate name and value against character set and size limit constraints.
// If IsValidCookieNameValuePair identifies that `name` and/or `value` are
// invalid, it will add an ExclusionReason to `status`.
ParsedCookie::IsValidCookieNameValuePair(name, value, status);
// Validate domain against character set and size limit constraints.
bool domain_is_valid = true;
if ((ParsedCookie::ParseValueString(domain) != domain)) {
status->AddExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_DOMAIN);
domain_is_valid = false;
}
if (!ParsedCookie::CookieAttributeValueHasValidCharSet(domain)) {
status->AddExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_DOMAIN);
domain_is_valid = false;
}
if (!ParsedCookie::CookieAttributeValueHasValidSize(domain)) {
status->AddExclusionReason(net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_ATTRIBUTE_VALUE_EXCEEDS_MAX_SIZE);
domain_is_valid = false;
}
const std::string& domain_attribute =
domain_is_valid ? domain : std::string();
std::optional<std::string> cookie_domain;
// This validation step must happen before GetCookieDomainWithString, so it
// doesn't fail DCHECKs.
if (!cookie_util::DomainIsHostOnly(url.host())) {
status->AddExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_DOMAIN);
} else if (cookie_domain = cookie_util::GetCookieDomainWithString(
url, domain_attribute, *status);
!cookie_domain) {
status->AddExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_DOMAIN);
}
// The next two sections set the source_scheme_ and source_port_. Normally
// these are taken directly from the url's scheme and port but if the url
// setting this cookie is considered a trustworthy origin then we may make
// some modifications. Note that here we assume that a trustworthy url must
// have a non-secure scheme (http). Since we can't know at this point if a url
// is trustworthy or not, we'll assume it is if the cookie is set with the
// `Secure` attribute.
//
// For both convenience and to try to match expectations, cookies that have
// the `Secure` attribute are modified to look like they were created by a
// secure url. This is helpful because this cookie can be treated like any
// other secure cookie when we're retrieving them and helps to prevent the
// cookie from getting "trapped" if the url loses trustworthiness.
CookieSourceScheme source_scheme = CookieSourceScheme::kNonSecure;
// This validation step must happen before SchemeIsCryptographic, so it
// doesn't fail DCHECKs.
if (!url.is_valid()) {
status->AddExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_DOMAIN);
} else {
// It's possible that a trustworthy origin is setting this cookie with the
// `Secure` attribute even if the url's scheme isn't secure. In that case
// we'll act like it was a secure scheme. This cookie will be rejected later
// if the url isn't allowed to access secure cookies so this isn't a
// problem.
source_scheme = (secure || url.SchemeIsCryptographic())
? CookieSourceScheme::kSecure
: CookieSourceScheme::kNonSecure;
if (source_scheme == CookieSourceScheme::kSecure &&
!url.SchemeIsCryptographic()) {
status->AddWarningReason(
CookieInclusionStatus::WarningReason::
WARN_TENTATIVELY_ALLOWING_SECURE_SOURCE_SCHEME);
}
}
// Get the port, this will get a default value if a port isn't explicitly
// provided. Similar to the source scheme, it's possible that a trustworthy
// origin is setting this cookie with the `Secure` attribute even if the url's
// scheme isn't secure. This function will return 443 to pretend like this
// cookie was set by a secure scheme.
int source_port =
CanonicalCookie::GetAndAdjustPortForTrustworthyUrls(url, secure);
std::string cookie_path = cookie_util::CanonPathWithString(url, path);
// Canonicalize path again to make sure it escapes characters as needed.
url::RawCanonOutputT<char> canon_path;
url::Component canon_path_component;
url::CanonicalizePath(cookie_path, &canon_path, &canon_path_component);
std::string_view encoded_cookie_path = canon_path.view().substr(
canon_path_component.begin, canon_path_component.len);
if (!path.empty()) {
if (cookie_path != path) {
// The path attribute was specified and found to be invalid, so record an
// error.
status->AddExclusionReason(net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_FAILURE_TO_STORE);
} else if (!ParsedCookie::CookieAttributeValueHasValidSize(
encoded_cookie_path)) {
// The path attribute was specified and encodes into a value that's longer
// than the length limit, so record an error.
status->AddExclusionReason(net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_ATTRIBUTE_VALUE_EXCEEDS_MAX_SIZE);
}
}
CookiePrefix prefix = cookie_util::GetCookiePrefix(name);
if (!cookie_util::IsCookiePrefixValid(prefix, url, secure, http_only,
domain_attribute, cookie_path)) {
status->AddExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX);
}
if (name == "" && HasHiddenPrefixName(value)) {
status->AddExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX);
}
if (!cookie_util::IsCookiePartitionedValid(
url, secure,
/*is_partitioned=*/partition_key.has_value(),
/*partition_has_nonce=*/
CookiePartitionKey::HasNonce(partition_key))) {
status->AddExclusionReason(net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_INVALID_PARTITIONED);
}
if (!last_access_time.is_null() && creation_time.is_null()) {
status->AddExclusionReason(
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_FAILURE_TO_STORE);
}
expiration_time = ValidateAndAdjustExpiryDate(expiration_time, creation_time,
source_scheme);
if (!status->IsInclude())
return nullptr;
auto cc = std::make_unique<CanonicalCookie>(
base::PassKey<CanonicalCookie>(), name, value,
std::move(cookie_domain).value_or(std::string()),
std::string(encoded_cookie_path), creation_time, expiration_time,
last_access_time,
/*last_update=*/base::Time::Now(), secure, http_only, same_site, priority,
partition_key, source_scheme, source_port, CookieSourceType::kOther);
if constexpr (DCHECK_IS_ON()) {
CanonicalCookie::CanonicalizationResult result = cc->IsCanonical();
DCHECK(result) << result;
}
return cc;
}
// static
std::unique_ptr<CanonicalCookie> CanonicalCookie::FromStorage(
std::string name,
std::string value,
std::string domain,
std::string path,
base::Time creation,
base::Time expiration,
base::Time last_access,
base::Time last_update,
bool secure,
bool httponly,
CookieSameSite same_site,
CookiePriority priority,
std::optional<CookiePartitionKey> partition_key,
CookieSourceScheme source_scheme,
int source_port,
CookieSourceType source_type) {
// We check source_port here because it could have concievably been
// corrupted and changed to out of range. Eventually this would be caught by
// IsCanonical*() but since the source_port is only used by metrics so far
// nothing else checks it. So let's normalize it here and then update this
// method when origin-bound cookies is implemented.
// TODO(crbug.com/40165805)
int validated_port = CookieBase::ValidateAndAdjustSourcePort(source_port);
auto cc = std::make_unique<CanonicalCookie>(
base::PassKey<CanonicalCookie>(), std::move(name), std::move(value),
std::move(domain), std::move(path), creation, expiration, last_access,
last_update, secure, httponly, same_site, priority, partition_key,
source_scheme, validated_port, source_type);
if (cc->IsCanonicalForFromStorage()) {
// This will help capture the number of times a cookie is canonical but does
// not have a valid name+value size length
bool valid_cookie_name_value_pair =
ParsedCookie::IsValidCookieNameValuePair(cc->Name(), cc->Value());
UMA_HISTOGRAM_BOOLEAN("Cookie.FromStorageWithValidLength",
valid_cookie_name_value_pair);
} else {
return nullptr;
}
return cc;
}
// static
std::unique_ptr<CanonicalCookie> CanonicalCookie::CreateUnsafeCookieForTesting(
const std::string& name,
const std::string& value,
const std::string& domain,
const std::string& path,
base::Time creation,
base::Time expiration,
base::Time last_access,
base::Time last_update,
bool secure,
bool httponly,
CookieSameSite same_site,
CookiePriority priority,
std::optional<CookiePartitionKey> partition_key,
CookieSourceScheme source_scheme,
int source_port,
CookieSourceType source_type) {
return std::make_unique<CanonicalCookie>(
base::PassKey<CanonicalCookie>(), name, value, domain, path, creation,
expiration, last_access, last_update, secure, httponly, same_site,
priority, partition_key, source_scheme, source_port, source_type);
}
// static
std::unique_ptr<CanonicalCookie> CanonicalCookie::CreateForTesting(
const GURL& url,
const std::string& cookie_line,
base::Time creation_time,
std::optional<base::Time> server_time,
std::optional<CookiePartitionKey> cookie_partition_key,
CookieSourceType source_type,
CookieInclusionStatus* status) {
return CanonicalCookie::Create(url, cookie_line, creation_time, server_time,
cookie_partition_key, source_type, status);
}
std::string CanonicalCookie::Value() const {
if (!value_.has_value()) {
return std::string();
}
return value_->value();
}
bool CanonicalCookie::IsEquivalentForSecureCookieMatching(
const CanonicalCookie& secure_cookie) const {
// Partition keys must both be equivalent.
bool same_partition_key = PartitionKey() == secure_cookie.PartitionKey();
// Names must be the same
bool same_name = Name() == secure_cookie.Name();
// They should domain-match in one direction or the other. (See RFC 6265bis
// section 5.1.3.)
// TODO(chlily): This does not check for the IP address case. This is bad due
// to https://crbug.com/1069935.
bool domain_match =
IsSubdomainOf(DomainWithoutDot(), secure_cookie.DomainWithoutDot()) ||
IsSubdomainOf(secure_cookie.DomainWithoutDot(), DomainWithoutDot());
bool path_match = secure_cookie.IsOnPath(Path());
bool equivalent_for_secure_cookie_matching =
same_partition_key && same_name && domain_match && path_match;
// IsEquivalent() is a stricter check than this.
DCHECK(!IsEquivalent(secure_cookie) || equivalent_for_secure_cookie_matching);
return equivalent_for_secure_cookie_matching;
}
bool CanonicalCookie::IsProbablyEquivalentTo(
const CanonicalCookie& other) const {
// LastUpdateDate is the most likely field to have changed.
return LastUpdateDate() == other.LastUpdateDate() &&
LastAccessDate() == other.LastAccessDate() &&
ExpiryDate() == other.ExpiryDate() &&
CreationDate() == other.CreationDate() &&
SecureAttribute() == other.SecureAttribute() &&
IsHttpOnly() == other.IsHttpOnly() && SameSite() == other.SameSite() &&
Priority() == other.Priority() &&
PartitionKey() == other.PartitionKey() && Name() == other.Name() &&
Domain() == other.Domain() && Path() == other.Path() &&
SourceScheme() == other.SourceScheme() &&
SourcePort() == other.SourcePort() &&
SourceType() == other.SourceType();
}
bool CanonicalCookie::HasEquivalentDataMembers(
const CanonicalCookie& other) const {
return IsProbablyEquivalentTo(other) && Value() == other.Value();
}
bool CanonicalCookie::IsWebEquivalentTo(const CanonicalCookie& other) const {
return IsEquivalent(other) && Value() == other.Value() &&
IsSecure() == other.IsSecure() && SameSite() == other.SameSite() &&
IsHttpOnly() == other.IsHttpOnly() &&
ExpiryDate() == other.ExpiryDate();
}
void CanonicalCookie::PostIncludeForRequestURL(
const CookieAccessResult& access_result,
const CookieOptions& options_used,
CookieOptions::SameSiteCookieContext::ContextType
cookie_inclusion_context_used) const {
if (!base::ShouldRecordSubsampledMetric(kHistogramSampleProbability)) {
return;
}
base::UmaHistogramEnumeration(
"Cookie.RequestSameSiteContext", cookie_inclusion_context_used,
CookieOptions::SameSiteCookieContext::ContextType::COUNT);
if (access_result.status.IsInclude()) {
base::UmaHistogramEnumeration(
"Cookie.IncludedRequestEffectiveSameSite.Subsampled",
access_result.effective_same_site, CookieEffectiveSameSite::COUNT);
}
using ContextRedirectTypeBug1221316 = CookieOptions::SameSiteCookieContext::
ContextMetadata::ContextRedirectTypeBug1221316;
ContextRedirectTypeBug1221316 redirect_type_for_metrics =
options_used.same_site_cookie_context()
.GetMetadataForCurrentSchemefulMode()
.redirect_type_bug_1221316;
if (redirect_type_for_metrics != ContextRedirectTypeBug1221316::kUnset) {
base::UmaHistogramEnumeration(
"Cookie.CrossSiteRedirectType.Read.Subsampled",
redirect_type_for_metrics);
}
if (access_result.status.HasWarningReason(
CookieInclusionStatus::WarningReason::
WARN_CROSS_SITE_REDIRECT_DOWNGRADE_CHANGES_INCLUSION)) {
base::UmaHistogramEnumeration(
"Cookie.CrossSiteRedirectDowngradeChangesInclusion2.Read",
CookieSameSiteToCookieSameSiteForMetrics(SameSite()));
}
}
void CanonicalCookie::PostIsSetPermittedInContext(
const CookieAccessResult& access_result,
const CookieOptions& options_used) const {
if (!base::ShouldRecordSubsampledMetric(kHistogramSampleProbability)) {
return;
}
if (access_result.status.IsInclude()) {
base::UmaHistogramEnumeration(
"Cookie.IncludedResponseEffectiveSameSite.Subsampled",
access_result.effective_same_site, CookieEffectiveSameSite::COUNT);
}
using ContextRedirectTypeBug1221316 = CookieOptions::SameSiteCookieContext::
ContextMetadata::ContextRedirectTypeBug1221316;
ContextRedirectTypeBug1221316 redirect_type_for_metrics =
options_used.same_site_cookie_context()
.GetMetadataForCurrentSchemefulMode()
.redirect_type_bug_1221316;
if (redirect_type_for_metrics != ContextRedirectTypeBug1221316::kUnset) {
base::UmaHistogramEnumeration(
"Cookie.CrossSiteRedirectType.Write.Subsampled",
redirect_type_for_metrics);
}
if (access_result.status.HasWarningReason(
CookieInclusionStatus::WarningReason::
WARN_CROSS_SITE_REDIRECT_DOWNGRADE_CHANGES_INCLUSION)) {
base::UmaHistogramEnumeration(
"Cookie.CrossSiteRedirectDowngradeChangesInclusion2.Write.Subsampled",
CookieSameSiteToCookieSameSiteForMetrics(SameSite()));
}
}
base::TimeDelta CanonicalCookie::GetLaxAllowUnsafeThresholdAge() const {
return base::FeatureList::IsEnabled(
features::kSameSiteDefaultChecksMethodRigorously)
? base::TimeDelta::Min()
: (base::FeatureList::IsEnabled(
features::kShortLaxAllowUnsafeThreshold)
? kShortLaxAllowUnsafeMaxAge
: kLaxAllowUnsafeMaxAge);
}
std::string CanonicalCookie::DebugString() const {
return base::StringPrintf(
"name: %s value: %s domain: %s path: %s creation: %" PRId64,
Name().c_str(), Value().c_str(), Domain().c_str(), Path().c_str(),
static_cast<int64_t>(CreationDate().ToTimeT()));
}
CanonicalCookie::CanonicalizationResult CanonicalCookie::IsCanonical() const {
// TODO(crbug.com/40787717) Eventually we should check the size of name+value,
// assuming we collect metrics and determine that a low percentage of cookies
// would fail this check. Note that we still don't want to enforce length
// checks on domain or path for the reason stated above.
// TODO(crbug.com/40800807): Eventually we should push this logic into
// IsCanonicalForFromStorage, but for now we allow cookies already stored with
// high expiration dates to be retrieved.
if (ValidateAndAdjustExpiryDate(expiry_date_, CreationDate(),
SourceScheme()) != expiry_date_) {
return Fail(CanonicalizationFailure::kInvalidExpiryDate);
}
return IsCanonicalForFromStorage();
}
CanonicalCookie::CanonicalizationResult
CanonicalCookie::IsCanonicalForFromStorage() const {
// Not checking domain or path against ParsedCookie as it may have
// come purely from the URL. Also, don't call IsValidCookieNameValuePair()
// here because we don't want to enforce the size checks on names or values
// that may have been reconstituted from the cookie store.
if (ParsedCookie::ParseTokenString(Name()) != Name()) {
return Fail(CanonicalizationFailure::kUnparseableName);
}
if (!ParsedCookie::ValueMatchesParsedValue(Value())) {
return Fail(CanonicalizationFailure::kUnparseableValue);
}
if (!ParsedCookie::IsValidCookieName(Name())) {
return Fail(CanonicalizationFailure::kInvalidName);
}
if (!ParsedCookie::IsValidCookieValue(Value())) {
return Fail(CanonicalizationFailure::kInvalidValue);
}
if (!last_access_date_.is_null() && CreationDate().is_null()) {
return Fail(
CanonicalizationFailure::kInconsistentCreationAndLastAccessDate);
}
// Check if name or value contains any non-ascii values, fail if they do.
if (base::FeatureList::IsEnabled(features::kDisallowNonAsciiCookies) &&
(!base::IsStringASCII(Name()) || !base::IsStringASCII(Value()))) {
return Fail(CanonicalizationFailure::kNonAsciiCharactersDisallowed);
}
url::CanonHostInfo canon_host_info;
std::string canonical_domain(CanonicalizeHost(Domain(), &canon_host_info));
// TODO(rdsmith): This specifically allows for empty domains. The spec
// suggests this is invalid (if a domain attribute is empty, the cookie's
// domain is set to the canonicalized request host; see
// https://tools.ietf.org/html/rfc6265#section-5.3). However, it is
// needed for Chrome extension cookies.
// Note: The above comment may be outdated. We should determine whether empty
// Domain() is ever valid and update this code accordingly.
// See http://crbug.com/730633 for more information.
if (canonical_domain != Domain()) {
return Fail(CanonicalizationFailure::kInvalidDomain);
}
if (Path().empty() || Path()[0] != '/') {
return Fail(CanonicalizationFailure::kInvalidPath);
}
CookiePrefix prefix = cookie_util::GetCookiePrefix(Name());
switch (prefix) {
case COOKIE_PREFIX_HOST:
if (!SecureAttribute() || Path() != "/" || Domain().empty() ||
Domain()[0] == '.') {
return Fail(CanonicalizationFailure::kInvalidHostPrefix);
}
break;
case COOKIE_PREFIX_SECURE:
if (!SecureAttribute()) {
return Fail(CanonicalizationFailure::kInvalidSecurePrefix);
}
break;
default:
break;
}
if (Name() == "" && HasHiddenPrefixName(Value())) {
return Fail(CanonicalizationFailure::kEmptyNameWithHiddenPrefix);
}
if (IsPartitioned() && !CookiePartitionKey::HasNonce(PartitionKey()) &&
!SecureAttribute()) {
return Fail(CanonicalizationFailure::kPartitionedInsecure);
}
return Pass();
}
bool CanonicalCookie::IsEffectivelySameSiteNone(
CookieAccessSemantics access_semantics) const {
return GetEffectiveSameSite(access_semantics) ==
CookieEffectiveSameSite::NO_RESTRICTION;
}
CookieEffectiveSameSite CanonicalCookie::GetEffectiveSameSiteForTesting(
CookieAccessSemantics access_semantics) const {
return GetEffectiveSameSite(access_semantics);
}
// static
std::string CanonicalCookie::BuildCookieLine(const CookieList& cookies) {
std::string cookie_line;
for (const auto& cookie : cookies) {
AppendCookieLineEntry(cookie, &cookie_line);
}
return cookie_line;
}
// static
std::string CanonicalCookie::BuildCookieLine(
const CookieAccessResultList& cookie_access_result_list) {
std::string cookie_line;
for (const auto& cookie_with_access_result : cookie_access_result_list) {
const CanonicalCookie& cookie = cookie_with_access_result.cookie;
AppendCookieLineEntry(cookie, &cookie_line);
}
return cookie_line;
}
// static
std::string CanonicalCookie::BuildCookieAttributesLine(
const CanonicalCookie& cookie) {
std::string cookie_line;
// In Mozilla, if you set a cookie like "AAA", it will have an empty token
// and a value of "AAA". When it sends the cookie back, it will send "AAA",
// so we need to avoid sending "=AAA" for a blank token value.
if (!cookie.Name().empty())
cookie_line += cookie.Name() + "=";
cookie_line += cookie.Value();
if (!cookie.Domain().empty())
cookie_line += "; domain=" + cookie.Domain();
if (!cookie.Path().empty())
cookie_line += "; path=" + cookie.Path();
if (cookie.ExpiryDate() != base::Time())
cookie_line += "; expires=" + HttpUtil::TimeFormatHTTP(cookie.ExpiryDate());
if (cookie.SecureAttribute()) {
cookie_line += "; secure";
}
if (cookie.IsHttpOnly())
cookie_line += "; httponly";
if (cookie.IsPartitioned() &&
!CookiePartitionKey::HasNonce(cookie.PartitionKey())) {
cookie_line += "; partitioned";
}
switch (cookie.SameSite()) {
case CookieSameSite::NO_RESTRICTION:
cookie_line += "; samesite=none";
break;
case CookieSameSite::LAX_MODE:
cookie_line += "; samesite=lax";
break;
case CookieSameSite::STRICT_MODE:
cookie_line += "; samesite=strict";
break;
case CookieSameSite::UNSPECIFIED:
// Don't append any text if the samesite attribute wasn't explicitly set.
break;
}
return cookie_line;
}
// static
int CanonicalCookie::GetAndAdjustPortForTrustworthyUrls(
const GURL& source_url,
bool url_is_trustworthy) {
// If the url isn't trustworthy, or if `source_url` is cryptographic then
// return the port of `source_url`.
if (!url_is_trustworthy || source_url.SchemeIsCryptographic()) {
return source_url.EffectiveIntPort();
}
// Only http and ws are cookieable schemes that have a port component. For
// both of these schemes their default port is 80 whereas their secure
// components have a default port of 443.
//
// Only in cases where we have an http/ws scheme with a default should we
// return 443.
if ((source_url.SchemeIs(url::kHttpScheme) ||
source_url.SchemeIs(url::kWsScheme)) &&
source_url.EffectiveIntPort() == 80) {
return 443;
}
// Different schemes, or non-default port values should keep the same port
// value.
return source_url.EffectiveIntPort();
}
// static
bool CanonicalCookie::HasHiddenPrefixName(std::string_view cookie_value) {
// Skip BWS as defined by HTTPSEM as SP or HTAB (0x20 or 0x9).
std::string_view value_without_BWS =
base::TrimString(cookie_value, " \t", base::TRIM_LEADING);
const std::string_view host_prefix = "__Host-";
// Compare the value to the host_prefix.
if (base::StartsWith(value_without_BWS, host_prefix,
base::CompareCase::INSENSITIVE_ASCII)) {
// This value contains a hidden prefix name.
return true;
}
// Do a similar check for the secure prefix
const std::string_view secure_prefix = "__Secure-";
if (base::StartsWith(value_without_BWS, secure_prefix,
base::CompareCase::INSENSITIVE_ASCII)) {
return true;
}
return false;
}
// static
CanonicalCookie::CanonicalizationResult CanonicalCookie::Pass() {
return CanonicalizationResult(base::PassKey<CanonicalCookie>(), std::nullopt);
}
// static
CanonicalCookie::CanonicalizationResult CanonicalCookie::Fail(
CanonicalCookie::CanonicalizationFailure failure) {
return CanonicalizationResult(base::PassKey<CanonicalCookie>(), failure);
}
CookieAndLineWithAccessResult::CookieAndLineWithAccessResult() = default;
CookieAndLineWithAccessResult::CookieAndLineWithAccessResult(
std::optional<CanonicalCookie> cookie,
std::string cookie_string,
CookieAccessResult access_result)
: cookie(std::move(cookie)),
cookie_string(std::move(cookie_string)),
access_result(access_result) {}
CookieAndLineWithAccessResult::CookieAndLineWithAccessResult(
const CookieAndLineWithAccessResult&) = default;
CookieAndLineWithAccessResult& CookieAndLineWithAccessResult::operator=(
const CookieAndLineWithAccessResult& cookie_and_line_with_access_result) =
default;
CookieAndLineWithAccessResult::CookieAndLineWithAccessResult(
CookieAndLineWithAccessResult&&) = default;
CookieAndLineWithAccessResult::~CookieAndLineWithAccessResult() = default;
std::ostream& operator<<(std::ostream& os,
CanonicalCookie::CanonicalizationFailure failure) {
os << [&]() -> std::string_view {
switch (failure) {
case CanonicalCookie::CanonicalizationFailure::kInvalidExpiryDate:
return "kInvalidExpiryDate";
case CanonicalCookie::CanonicalizationFailure::kUnparseableName:
return "kUnparseableName";
case CanonicalCookie::CanonicalizationFailure::kUnparseableValue:
return "kUnparseableValue";
case CanonicalCookie::CanonicalizationFailure::kInvalidName:
return "kInvalidName";
case CanonicalCookie::CanonicalizationFailure::kInvalidValue:
return "kInvalidValue";
case CanonicalCookie::CanonicalizationFailure::
kInconsistentCreationAndLastAccessDate:
return "kInconsistentCreationAndLastAccessDate";
case CanonicalCookie::CanonicalizationFailure::
kNonAsciiCharactersDisallowed:
return "kNonAsciiCharactersDisallowed";
case CanonicalCookie::CanonicalizationFailure::kInvalidDomain:
return "kInvalidDomain";
case CanonicalCookie::CanonicalizationFailure::kInvalidPath:
return "kInvalidPath";
case CanonicalCookie::CanonicalizationFailure::kInvalidHostPrefix:
return "kInvalidHostPrefix";
case CanonicalCookie::CanonicalizationFailure::kInvalidSecurePrefix:
return "kInvalidSecurePrefix";
case CanonicalCookie::CanonicalizationFailure::kEmptyNameWithHiddenPrefix:
return "kEmptyNameWithHiddenPrefix";
case CanonicalCookie::CanonicalizationFailure::kPartitionedInsecure:
return "kPartitionedInsecure";
}
NOTREACHED();
}();
return os;
}
} // namespace net