blob: 4e3eebc144ce9cf5268dd336bf1d936e1fa1a003 [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.
#ifndef NET_COOKIES_CANONICAL_COOKIE_H_
#define NET_COOKIES_CANONICAL_COOKIE_H_
#include <compare>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "base/types/pass_key.h"
#include "crypto/process_bound_string.h"
#include "net/base/net_export.h"
#include "net/cookies/cookie_access_result.h"
#include "net/cookies/cookie_base.h"
#include "net/cookies/cookie_constants.h"
#include "net/cookies/cookie_options.h"
#include "net/cookies/ref_unique_cookie_key.h"
#include "net/cookies/unique_cookie_key.h"
#include "url/third_party/mozilla/url_parse.h"
class GURL;
namespace base {
class Time;
} // namespace base
namespace net {
class ParsedCookie;
class CanonicalCookie;
class CookieInclusionStatus;
class CookiePartitionKey;
struct CookieWithAccessResult;
struct CookieAndLineWithAccessResult;
using CookieList = std::vector<CanonicalCookie>;
using CookieAndLineAccessResultList =
std::vector<CookieAndLineWithAccessResult>;
using CookieAccessResultList = std::vector<CookieWithAccessResult>;
// Represents the call sites of CanonicalCookie::FromStorage, used for metrics
// so we can better identify any callers that are not enforcing cookie name and
// value size limits as expected.
// LINT.IfChange(CanonicalCookieFromStorageCallSite)
enum class CanonicalCookieFromStorageCallSite {
kAndroidCookiesFetcherRestoreUtil,
kChromeOsCookieSyncConversions,
kOauthMultiloginResult,
kIosSystemCookieUtil,
kSqlitePersistentCookieStore,
kCookieManager,
kCookieManagerMojomTraits,
kRestrictedCookieManager,
kTests,
};
// LINT.ThenChange(/tools/metrics/histograms/metadata/cookie/histograms.xml:CanonicalCookieFromStorageCallSite)
// Represents a real/concrete cookie, which may be sent on requests or set by a
// response if the request context and attributes allow it.
class NET_EXPORT CanonicalCookie : public CookieBase {
public:
// Various reasons why `IsCanonical` and `IsCanonicalForFromStorage` can fail.
enum class CanonicalizationFailure {
kInvalidExpiryDate,
kUnparseableName,
kUnparseableValue,
kInvalidName,
kInvalidValue,
kInconsistentCreationAndLastAccessDate,
kNonAsciiCharactersDisallowed,
kInvalidDomain,
kInvalidPath,
kInvalidHostPrefix,
kInvalidSecurePrefix,
kEmptyNameWithHiddenPrefix,
kPartitionedInsecure,
};
// Carries metadata related to the canonicalization results for a given
// cookie.
class CanonicalizationResult {
public:
CanonicalizationResult() = delete;
CanonicalizationResult(base::PassKey<CanonicalCookie>,
std::optional<CanonicalizationFailure> failure);
friend bool operator==(const CanonicalizationResult&,
const CanonicalizationResult&) = default;
bool operator==(CanonicalizationFailure failure) const {
return failure_ == failure;
}
explicit operator bool() const { return !failure_.has_value(); }
NET_EXPORT friend std::ostream& operator<<(
std::ostream& os,
const CanonicalizationResult& result);
private:
std::optional<CanonicalizationFailure> failure_;
};
CanonicalCookie();
CanonicalCookie(const CanonicalCookie& other);
CanonicalCookie(CanonicalCookie&& other);
CanonicalCookie& operator=(const CanonicalCookie& other);
CanonicalCookie& operator=(CanonicalCookie&& other);
~CanonicalCookie() override;
// This constructor does not validate or canonicalize their inputs;
// the resulting CanonicalCookies should not be relied on to be canonical
// unless the caller has done appropriate validation and canonicalization
// themselves.
//
// NOTE: Prefer using Create, CreateSanitizedCookie, or FromStorage (depending
// on the use case) over directly using this constructor.
//
// NOTE: Do not add any defaults to this constructor, we want every caller to
// understand and choose their inputs.
CanonicalCookie(base::PassKey<CanonicalCookie>,
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 scheme_secure,
int source_port,
CookieSourceType source_type);
// Creates a new `CanonicalCookie` from the `cookie_line` and the
// `creation_time`. Canonicalizes inputs. May return nullptr if
// an attribute value is invalid. `url` must be valid. `creation_time` may
// not be null. Sets optional `status` to the relevant CookieInclusionStatus
// if provided. `server_time` indicates what the server sending us the Cookie
// thought the current time was when the cookie was produced. This is used to
// adjust for clock skew between server and host.
//
// SameSite and HttpOnly related parameters are not checked here,
// so creation of CanonicalCookies with e.g. SameSite=Strict from a cross-site
// context is allowed. Create() also does not check whether `url` has a secure
// scheme if attempting to create a Secure cookie. The Secure, SameSite, and
// HttpOnly related parameters should be checked when setting the cookie in
// the CookieStore.
//
// The partition_key argument only needs to be present if the cookie line
// contains the Partitioned attribute. If the cookie line will never contain
// that attribute, you should use std::nullopt to indicate you intend to
// always create an unpartitioned cookie. If partition_key has a value but the
// cookie line does not contain the Partitioned attribute, the resulting
// cookie will be unpartitioned. If the partition_key is null, then the cookie
// will be unpartitioned even when the cookie line has the Partitioned
// attribute.
//
// If a cookie is returned, `cookie->IsCanonical()` will be true.
//
// NOTE: Do not add any defaults to this constructor, we want every caller to
// understand and choose their inputs.
static std::unique_ptr<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);
// Create a canonical cookie based on sanitizing the passed inputs in the
// context of the passed URL. Returns a null unique pointer if the inputs
// cannot be sanitized. If `status` is provided it will have any relevant
// CookieInclusionStatus rejection reasons set. If a cookie is created,
// `cookie->IsCanonical()` will be true.
//
// NOTE: Do not add any defaults to this constructor, we want every caller to
// understand and choose their inputs.
static std::unique_ptr<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);
// FromStorage is a factory method which is meant for creating a new
// CanonicalCookie using properties of a previously existing cookie
// that was already ingested into the cookie store.
// This should NOT be used to create a new CanonicalCookie that was not
// already in the store.
// Returns nullptr if the resulting cookie is not canonical,
// i.e. cc->IsCanonical() returns false.
//
// NOTE: Do not add any defaults to this constructor, we want every caller to
// understand and choose their inputs.
static std::unique_ptr<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,
CanonicalCookieFromStorageCallSite call_site);
// Create a CanonicalCookie that is not guaranteed to actually be Canonical
// for tests. Use this only if you want to bypass parameter validation to
// create a cookie that otherwise shouldn't be possible to store.
static std::unique_ptr<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 = std::nullopt,
CookieSourceScheme scheme_secure = CookieSourceScheme::kUnset,
int source_port = url::PORT_UNSPECIFIED,
CookieSourceType source_type = CookieSourceType::kUnknown);
// Like Create but with some more friendly defaults for use in tests.
static std::unique_ptr<CanonicalCookie> CreateForTesting(
const GURL& url,
const std::string& cookie_line,
base::Time creation_time,
std::optional<base::Time> server_time = std::nullopt,
std::optional<CookiePartitionKey> cookie_partition_key = std::nullopt,
CookieSourceType source_type = CookieSourceType::kUnknown,
CookieInclusionStatus* status = nullptr);
friend auto operator<=>(const CanonicalCookie& left,
const CanonicalCookie& right) {
// Use the cookie properties that uniquely identify a cookie to determine
// ordering.
return left.RefUniqueKey() <=> right.RefUniqueKey();
}
friend bool operator==(const CanonicalCookie& left,
const CanonicalCookie& right) {
return left.RefUniqueKey() == right.RefUniqueKey();
}
// See CookieBase for other accessors.
std::string Value() const;
base::Time ExpiryDate() const { return expiry_date_; }
base::Time LastAccessDate() const { return last_access_date_; }
base::Time LastUpdateDate() const { return last_update_date_; }
bool IsPersistent() const { return !expiry_date_.is_null(); }
CookiePriority Priority() const { return priority_; }
CookieSourceType SourceType() const { return source_type_; }
bool IsExpired(base::Time current) const {
return !expiry_date_.is_null() && current >= expiry_date_;
}
// Are the cookies considered equivalent in the eyes of RFC 2965.
// The RFC says that name must match (case-sensitive), domain must
// match (case insensitive), and path must match (case sensitive).
// For the case insensitive domain compare, we rely on the domain
// having been canonicalized (in
// GetCookieDomainWithString->CanonicalizeHost).
// If partitioned cookies are enabled, then we check the cookies have the same
// partition key in addition to the checks in RFC 2965.
//
// To support origin-bound cookies the check will also include the source
// scheme and/or port depending on the state of the associated feature.
// Additionally, domain cookies get a slightly different check which does not
// include the source port.
bool IsEquivalent(const CanonicalCookie& ecc) const {
// It seems like it would make sense to take secure, httponly, and samesite
// into account, but the RFC doesn't specify this.
return RefUniqueKey() == ecc.RefUniqueKey();
}
// Checks a looser set of equivalency rules than 'IsEquivalent()' in order
// to support the stricter 'Secure' behaviors specified in Step 12 of
// https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05#section-5.4
// which originated from the proposal in
// https://tools.ietf.org/html/draft-ietf-httpbis-cookie-alone#section-3
//
// Returns 'true' if this cookie's name matches |secure_cookie|, and this
// cookie is a domain-match for |secure_cookie| (or vice versa), and
// |secure_cookie|'s path is "on" this cookie's path (as per
// 'IsOnPath()'). If partitioned cookies are enabled, it also checks that
// the cookie has the same partition key as |secure_cookie|.
//
// Note that while the domain-match cuts both ways (e.g. 'example.com'
// matches 'www.example.com' in either direction), the path-match is
// unidirectional (e.g. '/login/en' matches '/login' and '/', but
// '/login' and '/' do not match '/login/en').
//
// Conceptually:
// If new_cookie.IsEquivalentForSecureCookieMatching(secure_cookie) is
// true, this means that new_cookie would "shadow" secure_cookie: they
// would would be indistinguishable when serialized into a Cookie header.
// This is important because, if an attacker is attempting to set
// new_cookie, it should not be allowed to mislead the server into using
// new_cookie's value instead of secure_cookie's.
//
// The reason for the asymmetric path comparison ("cookie1=bad; path=/a/b"
// from an insecure source is not allowed if "cookie1=good; secure;
// path=/a" exists, but "cookie2=bad; path=/a" from an insecure source is
// allowed if "cookie2=good; secure; path=/a/b" exists) is because cookies
// in the Cookie header are serialized with longer path first. (See
// CookieSorter in cookie_monster.cc.) That is, they would be serialized
// as "Cookie: cookie1=bad; cookie1=good" in one case, and "Cookie:
// cookie2=good; cookie2=bad" in the other case. The first scenario is not
// allowed because the attacker injects the bad value, whereas the second
// scenario is ok because the good value is still listed first.
bool IsEquivalentForSecureCookieMatching(
const CanonicalCookie& secure_cookie) const;
// Returns true if the |other| cookie's data members match, except for the
// value. The heuristic is that, if the value changes, then `LastUpdateDate`
// is likely different.
bool IsProbablyEquivalentTo(const CanonicalCookie& other) const;
// Returns true if the |other| cookie's data members (instance variables)
// match, for comparing cookies in collections.
bool HasEquivalentDataMembers(const CanonicalCookie& other) const;
// Checks if a another cookie is equivalent to this one with respect to what
// information about cookies is revealed to the web platform.
//
// If true, it implies that the two cookies would appear identical to cookie
// change subscribers such as the CookieStore API or service workers.
bool IsWebEquivalentTo(const CanonicalCookie& other) const;
void SetLastAccessDate(base::Time date) { last_access_date_ = date; }
std::string DebugString() const;
// Returns a "null" time if expiration was unspecified or invalid.
static base::Time ParseExpiration(const ParsedCookie& pc,
base::Time current,
base::Time server_time);
// Per rfc6265bis the maximum expiry date is no further than 400 days in the
// future.
static base::Time ValidateAndAdjustExpiryDate(base::Time expiry_date,
base::Time creation_date,
net::CookieSourceScheme scheme);
// Return whether this object is a valid CanonicalCookie(). If the object is
// invalid, return the first reason found why not. Invalid cookies may be
// constructed by the detailed constructor.
// A cookie is considered canonical if-and-only-if:
// * It can be created by CanonicalCookie::Create, or
// * It is identical to a cookie created by CanonicalCookie::Create except
// that the creation time is null, or
// * It can be derived from a cookie created by CanonicalCookie::Create by
// entry into and retrieval from a cookie store (specifically, this means
// by the setting of an creation time in place of a null creation time, and
// the setting of a last access time).
// An additional requirement on a CanonicalCookie is that if the last
// access time is non-null, the creation time must also be non-null and
// greater than the last access time.
CanonicalizationResult IsCanonical() const;
// Return whether this object is a valid CanonicalCookie() when retrieving the
// cookie from the persistent store; and if the CanonicalCookie is invalid,
// return the first reason why not. Cookie that exist in the persistent store
// may have been created before more recent changes to the definition of
// "canonical". To ease the transition to the new definitions, and to prevent
// users from having their cookies deleted, this function supports the older
// definition of canonical. This function is intended to be temporary because
// as the number of older cookies (which are non-compliant with the newer
// definition of canonical) decay toward zero it can eventually be replaced by
// `IsCanonical()` to enforce the newer definition of canonical.
//
// A cookie is considered canonical by this function if-and-only-if:
// * It is considered canonical by IsCanonical()
// * TODO(crbug.com/40787717): Add exceptions once IsCanonical() starts
// enforcing them.
CanonicalizationResult IsCanonicalForFromStorage() const;
// Returns whether the effective SameSite mode is SameSite=None (i.e. no
// SameSite restrictions).
bool IsEffectivelySameSiteNone(CookieAccessSemantics access_semantics =
CookieAccessSemantics::UNKNOWN) const;
CookieEffectiveSameSite GetEffectiveSameSiteForTesting(
CookieAccessSemantics access_semantics =
CookieAccessSemantics::UNKNOWN) const;
// Returns the cookie line (e.g. "cookie1=value1; cookie2=value2") represented
// by |cookies|. The string is built in the same order as the given list.
static std::string BuildCookieLine(const CookieList& cookies);
// Same as above but takes a CookieAccessResultList
// (ignores the access result).
static std::string BuildCookieLine(const CookieAccessResultList& cookies);
// Takes a single CanonicalCookie and returns a cookie line containing the
// attributes of |cookie| formatted like a http set cookie header.
// (e.g. "cookie1=value1; domain=abc.com; path=/; secure").
static std::string BuildCookieAttributesLine(const CanonicalCookie& cookie);
private:
FRIEND_TEST_ALL_PREFIXES(CanonicalCookieTest,
TestGetAndAdjustPortForTrustworthyUrls);
FRIEND_TEST_ALL_PREFIXES(CanonicalCookieTest, TestHasHiddenPrefixName);
// Returns the appropriate port value for the given `source_url` depending on
// if the url is considered trustworthy or not.
//
// This function normally returns source_url.EffectiveIntPort(), but it can
// return a different port value if:
// * `source_url`'s scheme isn't cryptographically secure
// * `url_is_trustworthy` is true
// * `source_url`'s port is the default port for the scheme i.e.: 80
// If all these conditions are true then the returned value will be 443 to
// indicate that we're treating `source_url` as if it was secure.
static int GetAndAdjustPortForTrustworthyUrls(const GURL& source_url,
bool url_is_trustworthy);
// Checks for values that could be misinterpreted as a cookie name prefix.
static bool HasHiddenPrefixName(std::string_view cookie_value);
// Helpers for use in canonicalization checks.
static CanonicalizationResult Pass();
static CanonicalizationResult Fail(CanonicalizationFailure failure);
// CookieBase:
base::TimeDelta GetLaxAllowUnsafeThresholdAge() const override;
void PostIncludeForRequestURL(
const CookieAccessResult& access_result,
const CookieOptions& options_used,
CookieOptions::SameSiteCookieContext::ContextType
cookie_inclusion_context_used) const override;
void PostIsSetPermittedInContext(
const CookieAccessResult& access_result,
const CookieOptions& options_used) const override;
// Keep defaults here in sync with
// services/network/public/interfaces/cookie_manager.mojom.
// These are the fields specific to CanonicalCookie. See CookieBase for other
// data fields.
// If adding more data fields, please also adjust GetAllDataMembersAsTuple().
std::optional<crypto::ProcessBoundString> value_;
base::Time expiry_date_;
base::Time last_access_date_;
base::Time last_update_date_;
CookiePriority priority_{COOKIE_PRIORITY_MEDIUM};
CookieSourceType source_type_{CookieSourceType::kUnknown};
};
// Used to pass excluded cookie information when it's possible that the
// canonical cookie object may not be available.
struct NET_EXPORT CookieAndLineWithAccessResult {
CookieAndLineWithAccessResult();
CookieAndLineWithAccessResult(std::optional<CanonicalCookie> cookie,
std::string cookie_string,
CookieAccessResult access_result);
CookieAndLineWithAccessResult(
const CookieAndLineWithAccessResult& cookie_and_line_with_access_result);
CookieAndLineWithAccessResult& operator=(
const CookieAndLineWithAccessResult& cookie_and_line_with_access_result);
CookieAndLineWithAccessResult(
CookieAndLineWithAccessResult&& cookie_and_line_with_access_result);
~CookieAndLineWithAccessResult();
std::optional<CanonicalCookie> cookie;
std::string cookie_string;
CookieAccessResult access_result;
};
struct CookieWithAccessResult {
CanonicalCookie cookie;
CookieAccessResult access_result;
};
NET_EXPORT std::ostream& operator<<(
std::ostream& os,
CanonicalCookie::CanonicalizationFailure failure);
// Provided to allow gtest to create more helpful error messages, instead of
// printing hex.
inline void PrintTo(const CanonicalCookie& cc, std::ostream* os) {
*os << "{ name=" << cc.Name() << ", value=" << cc.Value() << " }";
}
inline void PrintTo(const CookieWithAccessResult& cwar, std::ostream* os) {
*os << "{ ";
PrintTo(cwar.cookie, os);
*os << ", ";
PrintTo(cwar.access_result, os);
*os << " }";
}
inline void PrintTo(const CookieAndLineWithAccessResult& calwar,
std::ostream* os) {
*os << "{ ";
if (calwar.cookie) {
PrintTo(*calwar.cookie, os);
} else {
*os << "nullopt";
}
*os << ", " << calwar.cookie_string << ", ";
PrintTo(calwar.access_result, os);
*os << " }";
}
} // namespace net
#endif // NET_COOKIES_CANONICAL_COOKIE_H_