blob: 2a9fbdcd4d64eb6ba8f9cbdf6f1bb258051cddfd [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.
// Implements common functionality for the Chrome Extensions Cookies API.
#include "chrome/browser/extensions/api/cookies/cookies_helpers.h"
#include <stddef.h>
#include <limits>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/extensions/api/cookies/cookies_api_constants.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/extensions/api/cookies.h"
#include "chrome/common/url_constants.h"
#include "content/public/browser/web_contents.h"
#include "extensions/common/extension.h"
#include "extensions/common/permissions/permissions_data.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_store.h"
#include "net/cookies/cookie_util.h"
#include "url/gurl.h"
using extensions::api::cookies::Cookie;
using extensions::api::cookies::CookieStore;
namespace GetAll = extensions::api::cookies::GetAll;
namespace extensions {
namespace {
void AppendCookieToVectorIfMatchAndHasHostPermission(
const net::CanonicalCookie cookie,
GetAll::Params::Details* details,
const Extension* extension,
std::vector<Cookie>* match_vector,
const net::CookiePartitionKeyCollection& cookie_partition_key_collection) {
// Ignore any cookie whose domain doesn't match the extension's
// host permissions.
GURL cookie_domain_url = cookies_helpers::GetURLFromCanonicalCookie(cookie);
if (!extension->permissions_data()->HasHostPermission(cookie_domain_url))
return;
// Filter the cookie using the match filter.
cookies_helpers::MatchFilter filter(details);
// There is an edge case where a getAll call that contains a
// partition key parameter but no top_level_site parameter results in a
// return of partitioned and non-partitioned cookies. To ensure this is
// handled correctly, the CookiePartitionKeyCollection value is set
filter.SetCookiePartitionKeyCollection(cookie_partition_key_collection);
if (filter.MatchesCookie(cookie)) {
match_vector->push_back(
cookies_helpers::CreateCookie(cookie, *details->store_id));
}
}
} // namespace
namespace cookies_helpers {
static const char kOriginalProfileStoreId[] = "0";
static const char kOffTheRecordProfileStoreId[] = "1";
Profile* ChooseProfileFromStoreId(const std::string& store_id,
Profile* profile,
bool include_incognito) {
DCHECK(profile);
bool allow_original = !profile->IsOffTheRecord();
bool allow_incognito = profile->IsOffTheRecord() ||
(include_incognito && profile->HasPrimaryOTRProfile());
if (store_id == kOriginalProfileStoreId && allow_original)
return profile->GetOriginalProfile();
if (store_id == kOffTheRecordProfileStoreId && allow_incognito)
return profile->GetPrimaryOTRProfile(/*create_if_needed=*/true);
return nullptr;
}
const char* GetStoreIdFromProfile(Profile* profile) {
DCHECK(profile);
return profile->IsOffTheRecord() ?
kOffTheRecordProfileStoreId : kOriginalProfileStoreId;
}
Cookie CreateCookie(const net::CanonicalCookie& canonical_cookie,
const std::string& store_id) {
Cookie cookie;
// A cookie is a raw byte sequence. By explicitly parsing it as UTF-8, we
// apply error correction, so the string can be safely passed to the renderer.
cookie.name = base::UTF16ToUTF8(base::UTF8ToUTF16(canonical_cookie.Name()));
cookie.value = base::UTF16ToUTF8(base::UTF8ToUTF16(canonical_cookie.Value()));
cookie.domain = canonical_cookie.Domain();
cookie.host_only =
net::cookie_util::DomainIsHostOnly(canonical_cookie.Domain());
// A non-UTF8 path is invalid, so we just replace it with an empty string.
cookie.path = base::IsStringUTF8(canonical_cookie.Path())
? canonical_cookie.Path()
: std::string();
cookie.secure = canonical_cookie.SecureAttribute();
cookie.http_only = canonical_cookie.IsHttpOnly();
switch (canonical_cookie.SameSite()) {
case net::CookieSameSite::NO_RESTRICTION:
cookie.same_site = api::cookies::SameSiteStatus::kNoRestriction;
break;
case net::CookieSameSite::LAX_MODE:
cookie.same_site = api::cookies::SameSiteStatus::kLax;
break;
case net::CookieSameSite::STRICT_MODE:
cookie.same_site = api::cookies::SameSiteStatus::kStrict;
break;
case net::CookieSameSite::UNSPECIFIED:
cookie.same_site = api::cookies::SameSiteStatus::kUnspecified;
break;
}
cookie.session = !canonical_cookie.IsPersistent();
if (canonical_cookie.IsPersistent()) {
double expiration_date =
canonical_cookie.ExpiryDate().InSecondsFSinceUnixEpoch();
if (canonical_cookie.ExpiryDate().is_max() ||
!std::isfinite(expiration_date)) {
expiration_date = std::numeric_limits<double>::max();
}
cookie.expiration_date = expiration_date;
}
cookie.store_id = store_id;
if (canonical_cookie.PartitionKey()) {
base::expected<net::CookiePartitionKey::SerializedCookiePartitionKey,
std::string>
serialized_partition_key =
net::CookiePartitionKey::Serialize(canonical_cookie.PartitionKey());
CHECK(serialized_partition_key.has_value());
cookie.partition_key = extensions::api::cookies::CookiePartitionKey();
// TODO (crbug.com/326605834) Once ancestor chain bit changes are
// implemented update this method utilize the ancestor bit.
cookie.partition_key->top_level_site =
serialized_partition_key->TopLevelSite();
}
return cookie;
}
CookieStore CreateCookieStore(Profile* profile, base::Value::List tab_ids) {
DCHECK(profile);
base::Value::Dict dict;
dict.Set(cookies_api_constants::kIdKey, GetStoreIdFromProfile(profile));
dict.Set(cookies_api_constants::kTabIdsKey, std::move(tab_ids));
auto cookie_store = CookieStore::FromValue(dict);
CHECK(cookie_store);
return std::move(cookie_store).value();
}
void GetCookieListFromManager(
network::mojom::CookieManager* manager,
const GURL& url,
const net::CookiePartitionKeyCollection& partition_key_collection,
network::mojom::CookieManager::GetCookieListCallback callback) {
manager->GetCookieList(url, net::CookieOptions::MakeAllInclusive(),
partition_key_collection, std::move(callback));
}
void GetAllCookiesFromManager(
network::mojom::CookieManager* manager,
network::mojom::CookieManager::GetAllCookiesCallback callback) {
manager->GetAllCookies(std::move(callback));
}
GURL GetURLFromCanonicalCookie(const net::CanonicalCookie& cookie) {
// This is only ever called for CanonicalCookies that have come from a
// CookieStore, which means they should not have an empty domain. Only file
// cookies are allowed to have empty domains, and those are only permitted on
// Android, and hopefully not for much longer (see crbug.com/582985).
DCHECK(!cookie.Domain().empty());
return net::cookie_util::CookieOriginToURL(cookie.Domain(),
cookie.SecureAttribute());
}
void AppendMatchingCookiesFromCookieListToVector(
const net::CookieList& all_cookies,
GetAll::Params::Details* details,
const Extension* extension,
std::vector<Cookie>* match_vector,
const net::CookiePartitionKeyCollection& cookie_partition_key_collection) {
for (const net::CanonicalCookie& cookie : all_cookies) {
AppendCookieToVectorIfMatchAndHasHostPermission(
cookie, details, extension, match_vector,
cookie_partition_key_collection);
}
}
void AppendMatchingCookiesFromCookieAccessResultListToVector(
const net::CookieAccessResultList& all_cookies_with_access_result,
GetAll::Params::Details* details,
const Extension* extension,
std::vector<Cookie>* match_vector) {
for (const net::CookieWithAccessResult& cookie_with_access_result :
all_cookies_with_access_result) {
const net::CanonicalCookie& cookie = cookie_with_access_result.cookie;
AppendCookieToVectorIfMatchAndHasHostPermission(
cookie, details, extension, match_vector,
CookiePartitionKeyCollectionFromApiPartitionKey(
details->partition_key));
}
}
void AppendToTabIdList(Browser* browser, base::Value::List& tab_ids) {
DCHECK(browser);
TabStripModel* tab_strip = browser->tab_strip_model();
for (int i = 0; i < tab_strip->count(); ++i) {
tab_ids.Append(ExtensionTabUtil::GetTabId(tab_strip->GetWebContentsAt(i)));
}
}
bool ValidateCookieApiPartitionKey(
const std::optional<extensions::api::cookies::CookiePartitionKey>&
partition_key,
std::optional<net::CookiePartitionKey>& net_partition_key,
std::string& error_message) {
// TODO (crbug.com/326605834) Once ancestor chain bit changes are
// implemented update this method utilize the ancestor bit.
if (partition_key.has_value() && partition_key->top_level_site.has_value() &&
!partition_key->top_level_site->empty()) {
base::expected<net::CookiePartitionKey, std::string> key =
net::CookiePartitionKey::FromUntrustedInput(
partition_key->top_level_site.value(),
/*has_cross_site_ancestor=*/true);
if (!key.has_value()) {
error_message = key.error();
return false;
}
net_partition_key = key.value();
// Record 'well formatted' uma here so that we count only coercible
// partition keys.
base::UmaHistogramBoolean(
"Extensions.CookieAPIPartitionKeyWellFormatted",
net::SchemefulSite::Deserialize(partition_key->top_level_site.value())
.Serialize() == partition_key->top_level_site.value());
}
return true;
}
bool CookieMatchesPartitionKeyCollection(
const net::CookiePartitionKeyCollection& cookie_partition_key_collection,
const net::CanonicalCookie& cookie) {
if (!cookie.IsPartitioned()) {
return cookie_partition_key_collection.ContainsAllKeys() ||
cookie_partition_key_collection.IsEmpty();
}
return cookie_partition_key_collection.Contains(*cookie.PartitionKey());
}
bool CanonicalCookiePartitionKeyMatchesApiCookiePartitionKey(
const std::optional<extensions::api::cookies::CookiePartitionKey>&
api_partition_key,
const std::optional<net::CookiePartitionKey>& net_partition_key) {
if (!api_partition_key.has_value()) {
return !net_partition_key.has_value();
}
if (!net_partition_key.has_value()) {
return false;
}
// If both keys are present, they both must be serializable for a match.
if (!net_partition_key->IsSerializeable() ||
!api_partition_key->top_level_site.has_value()) {
return false;
}
// TODO (crbug.com/326605834) Once ancestor chain bit changes are
// implemented update this method utilize the ancestor bit.
base::expected<net::CookiePartitionKey::SerializedCookiePartitionKey,
std::string>
net_serialized_result =
net::CookiePartitionKey::Serialize(net_partition_key);
if (!net_serialized_result.has_value()) {
return false;
}
return net_serialized_result->TopLevelSite() ==
api_partition_key->top_level_site.value();
}
net::CookiePartitionKeyCollection
CookiePartitionKeyCollectionFromApiPartitionKey(
const std::optional<extensions::api::cookies::CookiePartitionKey>&
partition_key) {
if (!partition_key) {
return net::CookiePartitionKeyCollection();
}
if (!partition_key->top_level_site) {
return net::CookiePartitionKeyCollection::ContainsAll();
}
if (partition_key->top_level_site.value().empty()) {
return net::CookiePartitionKeyCollection();
}
// TODO (crbug.com/326605834) Once ancestor chain bit changes are implemented
// update this method utilize the ancestor bit.
base::expected<net::CookiePartitionKey, std::string> net_partition_key =
net::CookiePartitionKey::FromUntrustedInput(
partition_key->top_level_site.value(),
/*has_cross_site_ancestor=*/true);
if (!net_partition_key.has_value()) {
return net::CookiePartitionKeyCollection();
}
return net::CookiePartitionKeyCollection::FromOptional(
net_partition_key.value());
}
MatchFilter::MatchFilter(GetAll::Params::Details* details) : details_(details) {
DCHECK(details_);
}
bool MatchFilter::MatchesCookie(
const net::CanonicalCookie& cookie) {
if (!CookieMatchesPartitionKeyCollection(cookie_partition_key_collection_,
cookie)) {
return false;
}
// Confirm there's at least one parameter to check.
if (!details_->name && !details_->domain && !details_->path &&
!details_->secure && !details_->session && !details_->partition_key) {
return true;
}
if (details_->name && *details_->name != cookie.Name())
return false;
if (!MatchesDomain(cookie.Domain()))
return false;
if (details_->path && *details_->path != cookie.Path())
return false;
if (details_->secure && *details_->secure != cookie.SecureAttribute()) {
return false;
}
if (details_->session && *details_->session != !cookie.IsPersistent())
return false;
return true;
}
void MatchFilter::SetCookiePartitionKeyCollection(
const net::CookiePartitionKeyCollection& cookie_partition_key_collection) {
cookie_partition_key_collection_ = cookie_partition_key_collection;
}
bool MatchFilter::MatchesDomain(const std::string& domain) {
if (!details_->domain)
return true;
// Add a leading '.' character to the filter domain if it doesn't exist.
if (net::cookie_util::DomainIsHostOnly(*details_->domain))
details_->domain->insert(0, ".");
std::string sub_domain(domain);
// Strip any leading '.' character from the input cookie domain.
if (!net::cookie_util::DomainIsHostOnly(sub_domain))
sub_domain = sub_domain.substr(1);
// Now check whether the domain argument is a subdomain of the filter domain.
for (sub_domain.insert(0, ".");
sub_domain.length() >= details_->domain->length();) {
if (sub_domain == *details_->domain)
return true;
const size_t next_dot = sub_domain.find('.', 1); // Skip over leading dot.
sub_domain.erase(0, next_dot);
}
return false;
}
} // namespace cookies_helpers
} // namespace extensions