blob: b3435012069b85f821b6dc03061e0a2ca1e81461 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/device_bound_sessions/cookie_craving.h"
#include <optional>
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_set.h"
#include "base/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.h"
#include "base/strings/strcat.h"
#include "net/base/url_util.h"
#include "net/cookies/canonical_cookie.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_util.h"
#include "net/cookies/parsed_cookie.h"
#include "net/device_bound_sessions/proto/storage.pb.h"
#include "net/url_request/url_request.h"
#include "url/url_canon.h"
namespace net::device_bound_sessions {
namespace {
// A one-character value suffices to be non-empty. We avoid using an
// unnecessarily long placeholder so as to not eat into the 4096-char limit for
// a cookie name-value pair.
const char kPlaceholderValue[] = "v";
proto::CookieSameSite ProtoEnumFromCookieSameSite(CookieSameSite same_site) {
switch (same_site) {
case CookieSameSite::UNSPECIFIED:
return proto::CookieSameSite::COOKIE_SAME_SITE_UNSPECIFIED;
case CookieSameSite::NO_RESTRICTION:
return proto::CookieSameSite::NO_RESTRICTION;
case CookieSameSite::LAX_MODE:
return proto::CookieSameSite::LAX_MODE;
case CookieSameSite::STRICT_MODE:
return proto::CookieSameSite::STRICT_MODE;
}
}
CookieSameSite CookieSameSiteFromProtoEnum(proto::CookieSameSite proto) {
switch (proto) {
case proto::CookieSameSite::COOKIE_SAME_SITE_UNSPECIFIED:
return CookieSameSite::UNSPECIFIED;
case proto::CookieSameSite::NO_RESTRICTION:
return CookieSameSite::NO_RESTRICTION;
case proto::CookieSameSite::LAX_MODE:
return CookieSameSite::LAX_MODE;
case proto::CookieSameSite::STRICT_MODE:
return CookieSameSite::STRICT_MODE;
}
}
proto::CookieSourceScheme ProtoEnumFromCookieSourceScheme(
CookieSourceScheme scheme) {
switch (scheme) {
case CookieSourceScheme::kUnset:
return proto::CookieSourceScheme::UNSET;
case CookieSourceScheme::kNonSecure:
return proto::CookieSourceScheme::NON_SECURE;
case CookieSourceScheme::kSecure:
return proto::CookieSourceScheme::SECURE;
}
}
CookieSourceScheme CookieSourceSchemeFromProtoEnum(
proto::CookieSourceScheme proto) {
switch (proto) {
case proto::CookieSourceScheme::UNSET:
return CookieSourceScheme::kUnset;
case proto::CookieSourceScheme::NON_SECURE:
return CookieSourceScheme::kNonSecure;
case proto::CookieSourceScheme::SECURE:
return CookieSourceScheme::kSecure;
}
}
} // namespace
// static
std::optional<CookieCraving> CookieCraving::Create(
const GURL& url,
const std::string& name,
const std::string& attributes,
base::Time creation_time) {
if (!url.is_valid() || creation_time.is_null()) {
return std::nullopt;
}
// Check the name first individually, otherwise the next step which cobbles
// together a cookie line may mask issues with the name.
if (!ParsedCookie::IsValidCookieName(name)) {
return std::nullopt;
}
// Construct an imitation "Set-Cookie" line to feed into ParsedCookie.
// Make up a value which is an arbitrary a non-empty string, because the
// "value" of the ParsedCookie will be discarded anyway, and it is valid for
// a cookie's name to be empty, but not for both name and value to be empty.
std::string line_to_parse =
base::StrCat({name, "=", kPlaceholderValue, ";", attributes});
ParsedCookie parsed_cookie(line_to_parse);
if (!parsed_cookie.IsValid()) {
return std::nullopt;
}
static constexpr auto kPermittedAttributes =
base::MakeFixedFlatSet<std::string>(
{"domain", "path", "secure", "httponly", "samesite"});
if (!parsed_cookie.ForEachAttribute(
[](std::string_view attribute, std::string_view value) {
return base::Contains(kPermittedAttributes, attribute);
})) {
return std::nullopt;
}
// `domain` is the domain key for storing the CookieCraving, determined
// from the domain attribute value (if any) and the URL. A domain cookie is
// marked by a preceding dot, as per CookieBase::Domain(), whereas a host
// cookie has no leading dot.
std::string domain_attribute_value;
if (parsed_cookie.Domain()) {
domain_attribute_value = parsed_cookie.Domain().value();
}
CookieInclusionStatus ignored_status;
std::optional<std::string> domain = cookie_util::GetCookieDomainWithString(
url, domain_attribute_value, ignored_status);
// Note: This is a deviation from CanonicalCookie. Here, we also require that
// domain is non-empty, which CanonicalCookie does not. See comment below in
// IsValid().
if (!domain || domain->empty()) {
return std::nullopt;
}
std::string path =
cookie_util::CanonPathWithString(url, parsed_cookie.Path().value_or(""));
CookiePrefix prefix = cookie_util::GetCookiePrefix(name);
if (!cookie_util::IsCookiePrefixValid(prefix, url, parsed_cookie)) {
return std::nullopt;
}
// Note: This is a deviation from CanonicalCookie::Create(), which allows
// cookies with a Secure attribute to be created as if they came from a
// cryptographic URL, even if the URL is not cryptographic, on the basis that
// the URL might be trustworthy. CookieCraving makes the simplifying
// assumption to ignore this case.
CookieSourceScheme source_scheme = url.SchemeIsCryptographic()
? CookieSourceScheme::kSecure
: CookieSourceScheme::kNonSecure;
int source_port = url.EffectiveIntPort();
CookieCraving cookie_craving{parsed_cookie.Name(),
std::move(domain).value(),
std::move(path),
creation_time,
parsed_cookie.IsSecure(),
parsed_cookie.IsHttpOnly(),
parsed_cookie.SameSite().first,
source_scheme,
source_port};
CHECK(cookie_craving.IsValid());
CookieInclusionStatus status;
if (!cookie_craving.CreateCanonicalCookieForRequest(url, &status)) {
SCOPED_CRASH_KEY_STRING256("CookieCraving", "Create",
status.GetDebugString());
base::debug::DumpWithoutCrashing();
// If we're not able to create a canonical cookie here, then we likely won't
// be able to in `CookieCraving::ShouldIncludeForRequest` later, so there's
// no point in creating the craving.
// TODO(crbug.com/435221694): See related TODO below for plan for
// longer-term fix.
return std::nullopt;
}
return cookie_craving;
}
// TODO(crbug.com/438792839): Much of this function is copied directly from CanonicalCookie.
// Try to deduplicate it.
bool CookieCraving::IsValid() const {
if (ParsedCookie::ParseTokenString(Name()) != Name() ||
!ParsedCookie::IsValidCookieName(Name())) {
return false;
}
if (CreationDate().is_null()) {
return false;
}
url::CanonHostInfo ignored_info;
std::string canonical_domain = CanonicalizeHost(Domain(), &ignored_info);
// Note: This is a deviation from CanonicalCookie. CookieCraving does not
// allow Domain() to be empty, whereas CanonicalCookie does (perhaps
// erroneously).
if (Domain().empty() || Domain() != canonical_domain) {
return false;
}
if (Path().empty() || Path().front() != '/') {
return false;
}
CookiePrefix prefix = cookie_util::GetCookiePrefix(Name());
switch (prefix) {
case COOKIE_PREFIX_HOST:
if (!SecureAttribute() || Path() != "/" || !IsHostCookie()) {
return false;
}
break;
case COOKIE_PREFIX_SECURE:
if (!SecureAttribute()) {
return false;
}
break;
case COOKIE_PREFIX_HTTP:
if (!SecureAttribute() || !IsHttpOnly()) {
return false;
}
break;
case COOKIE_PREFIX_HOSTHTTP:
if (!SecureAttribute() || Path() != "/" || !IsHostCookie() ||
!IsHttpOnly()) {
return false;
}
break;
case COOKIE_PREFIX_NONE:
case COOKIE_PREFIX_LAST:
break;
}
if (IsPartitioned()) {
if (CookiePartitionKey::HasNonce(PartitionKey())) {
return true;
}
if (!SecureAttribute()) {
return false;
}
}
return true;
}
bool CookieCraving::IsSatisfiedBy(
const CanonicalCookie& canonical_cookie) const {
CHECK(IsValid());
CHECK(canonical_cookie.IsCanonicalForFromStorage());
// Note: Creation time is not required to match. DBSC configs may be set at
// different times from the cookies they reference. DBSC also does not require
// expiry time to match, for similar reasons. Source scheme and port are also
// not required to match. DBSC does not require the config and its required
// cookie to come from the same URL (and the source host does not matter as
// long as the Domain attribute value matches), so it doesn't make sense to
// compare the source scheme and port either.
// TODO(crbug.com/353772143): Decide more carefully how nonced partition keys
// should be compared.
auto make_required_members_tuple = [](const CookieBase& c) {
return std::make_tuple(c.Name(), c.Domain(), c.Path(), c.SecureAttribute(),
c.IsHttpOnly(), c.SameSite(), c.PartitionKey());
};
return make_required_members_tuple(*this) ==
make_required_members_tuple(canonical_cookie);
}
std::string CookieCraving::DebugString() const {
auto bool_to_string = [](bool b) { return b ? "true" : "false"; };
return base::StrCat({"Name: ", Name(), "; Domain: ", Domain(),
"; Path: ", Path(),
"; SecureAttribute: ", bool_to_string(SecureAttribute()),
"; IsHttpOnly: ", bool_to_string(IsHttpOnly()),
"; SameSite: ", CookieSameSiteToString(SameSite()),
"; IsPartitioned: ", bool_to_string(IsPartitioned())});
// Source scheme and port, and creation date omitted for brevity.
}
// static
CookieCraving CookieCraving::CreateUnsafeForTesting(
std::string name,
std::string domain,
std::string path,
base::Time creation,
bool secure,
bool httponly,
CookieSameSite same_site,
CookieSourceScheme source_scheme,
int source_port) {
return CookieCraving{
std::move(name), std::move(domain), std::move(path), creation, secure,
httponly, same_site, source_scheme, source_port};
}
CookieCraving::CookieCraving() = default;
CookieCraving::CookieCraving(std::string name,
std::string domain,
std::string path,
base::Time creation,
bool secure,
bool httponly,
CookieSameSite same_site,
CookieSourceScheme source_scheme,
int source_port)
: CookieBase(std::move(name),
std::move(domain),
std::move(path),
creation,
secure,
httponly,
same_site,
/*partition_key=*/std::nullopt,
source_scheme,
source_port) {}
CookieCraving::CookieCraving(const CookieCraving& other) = default;
CookieCraving::CookieCraving(CookieCraving&& other) = default;
CookieCraving& CookieCraving::operator=(const CookieCraving& other) = default;
CookieCraving& CookieCraving::operator=(CookieCraving&& other) = default;
CookieCraving::~CookieCraving() = default;
bool CookieCraving::IsEqualForTesting(const CookieCraving& other) const {
return Name() == other.Name() && Domain() == other.Domain() &&
Path() == other.Path() &&
SecureAttribute() == other.SecureAttribute() &&
IsHttpOnly() == other.IsHttpOnly() && SameSite() == other.SameSite() &&
SourceScheme() == other.SourceScheme() &&
SourcePort() == other.SourcePort() &&
CreationDate() == other.CreationDate() &&
PartitionKey() == other.PartitionKey();
}
std::ostream& operator<<(std::ostream& os, const CookieCraving& cc) {
os << cc.DebugString();
return os;
}
proto::CookieCraving CookieCraving::ToProto() const {
CHECK(IsValid());
proto::CookieCraving proto;
proto.set_name(Name());
proto.set_domain(Domain());
proto.set_path(Path());
proto.set_secure(SecureAttribute());
proto.set_httponly(IsHttpOnly());
proto.set_source_port(SourcePort());
proto.set_creation_time(
CreationDate().ToDeltaSinceWindowsEpoch().InMicroseconds());
proto.set_same_site(ProtoEnumFromCookieSameSite(SameSite()));
proto.set_source_scheme(ProtoEnumFromCookieSourceScheme(SourceScheme()));
return proto;
}
// static
std::optional<CookieCraving> CookieCraving::CreateFromProto(
const proto::CookieCraving& proto) {
if (!proto.has_name() || !proto.has_domain() || !proto.has_path() ||
!proto.has_secure() || !proto.has_httponly() ||
!proto.has_source_port() || !proto.has_creation_time() ||
!proto.has_same_site() || !proto.has_source_scheme()) {
return std::nullopt;
}
CookieCraving cookie_craving{
proto.name(),
proto.domain(),
proto.path(),
base::Time::FromDeltaSinceWindowsEpoch(
base::Microseconds(proto.creation_time())),
proto.secure(),
proto.httponly(),
CookieSameSiteFromProtoEnum(proto.same_site()),
CookieSourceSchemeFromProtoEnum(proto.source_scheme()),
proto.source_port()};
if (!cookie_craving.IsValid()) {
return std::nullopt;
}
return cookie_craving;
}
bool CookieCraving::ShouldIncludeForRequest(
URLRequest* request,
const FirstPartySetMetadata& first_party_set_metadata,
const CookieOptions& options,
const CookieAccessParams& params) const {
if (!IncludeForRequestURL(request->url(), options, params)
.status.IsInclude()) {
return false;
}
CookieInclusionStatus status;
std::unique_ptr<CanonicalCookie> canonical_cookie =
CreateCanonicalCookieForRequest(request->url(), &status);
if (!canonical_cookie) {
SCOPED_CRASH_KEY_STRING256("CookieCraving", "ShouldInclude",
status.GetDebugString());
base::debug::DumpWithoutCrashing();
// If we're not able to create a canonical cookie here, return false instead
// of crashing below.
// TODO(crbug.com/435221694): See related TODO below for plan for
// longer-term fix.
return false;
}
CookieAccessResultList included_cravings;
included_cravings.emplace_back(std::move(*canonical_cookie));
CookieAccessResultList excluded_cravings;
return request->network_delegate()->AnnotateAndMoveUserBlockedCookies(
*request, first_party_set_metadata, included_cravings, excluded_cravings);
}
bool CookieCraving::CanSetBoundCookie(
const URLRequest& request,
const FirstPartySetMetadata& first_party_set_metadata,
CookieOptions* options) const {
// TODO(crbug.com/438783631): Refactor this.
// The below is all copied from
// UrlRequestHttpJob::SaveCookiesAndNotifyHeadersComplete. We should refactor
// it.
CookieInclusionStatus status;
std::unique_ptr<CanonicalCookie> canonical_cookie =
CreateCanonicalCookieForRequest(request.url(), &status);
if (!canonical_cookie || !status.IsInclude()) {
return false;
}
if (!request.network_delegate()) {
return false;
}
if (!request.network_delegate()->CanSetCookie(
request, *canonical_cookie, options, first_party_set_metadata,
&status)) {
return false;
}
return IsSetPermittedInContext(
request.url(), *options,
CookieAccessParams(CookieAccessSemantics::UNKNOWN,
CookieScopeSemantics::UNKNOWN,
/* delegate_treats_url_as_trustworthy=*/false),
{"https"}, std::nullopt)
.status.IsInclude();
}
std::unique_ptr<CanonicalCookie> CookieCraving::CreateCanonicalCookieForRequest(
const GURL& url,
CookieInclusionStatus* status) const {
// The `NetworkDelegate` can also reject cookies for any reason
// (e.g. user preferences). So we need to synthesize a
// `CanonicalCookie` and make sure it would be included to check those
// conditions too.
base::Time now = base::Time::Now();
std::string domain = Domain();
// This fix is needed because non-IP address __Host- prefix cookies are
// considered invalid if they pass through a domain, but Domain() is defined
// even for __Host- prefix cookies. This fix is very limited in scope for now
// (only __Host- prefix cookies).
// TODO(crbug.com/435221694): re-implement the way we call into
// `AnnotateAndMoveUserBlockedCookies` so that it is not possible for a
// validation to fail in this method. Some ideas:
// 1) Is it needed for `CookieCraving` creation validation and
// `CanonicalCookie` creation validation to be different in the first
// place?
// 2) Can we refactor `AnnotateAndMoveUserBlockedCookies` to input a
// `CookieBase` instead?
if (!url.HostIsIPAddress() &&
cookie_util::GetCookiePrefix(Name()) == COOKIE_PREFIX_HOST) {
domain = "";
}
return CanonicalCookie::CreateSanitizedCookie(
url, Name(), /*value=*/"", domain, Path(), CreationDate(),
now + base::Days(1), now, IsSecure(), IsHttpOnly(), SameSite(),
COOKIE_PRIORITY_DEFAULT, PartitionKey(), status);
}
} // namespace net::device_bound_sessions