|  | // 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/extension_tab_util.h" | 
|  | #include "chrome/browser/extensions/window_controller.h" | 
|  | #include "chrome/browser/profiles/profile.h" | 
|  | #include "chrome/common/extensions/api/cookies.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_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 { | 
|  |  | 
|  | constexpr char kIdKey[] = "id"; | 
|  | constexpr char kTabIdsKey[] = "tabIds"; | 
|  |  | 
|  | 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(); | 
|  | cookie.partition_key->top_level_site = | 
|  | serialized_partition_key->TopLevelSite(); | 
|  | cookie.partition_key->has_cross_site_ancestor = | 
|  | serialized_partition_key->has_cross_site_ancestor(); | 
|  | } | 
|  | return cookie; | 
|  | } | 
|  |  | 
|  | CookieStore CreateCookieStore(Profile* profile, base::Value::List tab_ids) { | 
|  | DCHECK(profile); | 
|  | base::Value::Dict dict; | 
|  | dict.Set(kIdKey, GetStoreIdFromProfile(profile)); | 
|  | dict.Set(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(WindowController* window, base::Value::List& tab_ids) { | 
|  | for (int i = 0; i < window->GetTabCount(); ++i) { | 
|  | tab_ids.Append(ExtensionTabUtil::GetTabId(window->GetWebContentsAt(i))); | 
|  | } | 
|  | } | 
|  |  | 
|  | base::expected<bool, std::string> CalculateHasCrossSiteAncestor( | 
|  | const std::string& url_string, | 
|  | std::optional<extensions::api::cookies::CookiePartitionKey>& | 
|  | partition_key) { | 
|  | // Can not calculate hasCrossSiteAncestor for a key that is not present or | 
|  | // does not have a top_level_site. | 
|  | CHECK(partition_key.has_value() || | 
|  | !partition_key->top_level_site.has_value()); | 
|  |  | 
|  | if (partition_key->has_cross_site_ancestor.has_value()) { | 
|  | return base::ok(partition_key->has_cross_site_ancestor.value()); | 
|  | } | 
|  |  | 
|  | // Empty top_level_site indicates an unpartitioned cookie which always has a | 
|  | // hasCrossSiteAncestor of false. | 
|  | if (partition_key->top_level_site.value().empty()) { | 
|  | return base::ok(false); | 
|  | } | 
|  |  | 
|  | GURL url = GURL(url_string); | 
|  | if (!url.is_valid()) { | 
|  | return base::unexpected("Invalid url_string."); | 
|  | } | 
|  |  | 
|  | GURL top_level_site = GURL(partition_key->top_level_site.value()); | 
|  | if (!top_level_site.is_valid()) { | 
|  | return base::unexpected( | 
|  | "Invalid value for CookiePartitionKey.topLevelSite."); | 
|  | } | 
|  |  | 
|  | return !net::SiteForCookies::FromUrl(url).IsFirstParty(top_level_site); | 
|  | } | 
|  |  | 
|  | bool ValidateCrossSiteAncestor( | 
|  | const std::string& url_string, | 
|  | const std::optional<extensions::api::cookies::CookiePartitionKey>& | 
|  | partition_key, | 
|  | std::string* error_out) { | 
|  | // Unpartitioned cookie has no value to validate. | 
|  | if (!partition_key.has_value()) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // A has_cross_site_ancestor value cannot be valid without a top_level_site. | 
|  | if (!partition_key->top_level_site.has_value()) { | 
|  | if (partition_key->has_cross_site_ancestor.has_value()) { | 
|  | *error_out = | 
|  | "CookiePartitionKey.topLevelSite is not present when " | 
|  | "CookiePartitionKey.hasCrossSiteAncestor is present."; | 
|  | return false; | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // Empty top_level_site indicates an unpartitioned cookie which must have a | 
|  | // value of false. | 
|  | if (partition_key->top_level_site.value().empty()) { | 
|  | if (partition_key->has_cross_site_ancestor.has_value() && | 
|  | partition_key->has_cross_site_ancestor.value()) { | 
|  | *error_out = "CookiePartitionKey.hasCrossSiteAncestor is invalid."; | 
|  | return false; | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | if (!partition_key->has_cross_site_ancestor.has_value()) { | 
|  | *error_out = | 
|  | "Can not validate an empty value for " | 
|  | "hasCrossSiteAncestor."; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | GURL url = GURL(url_string); | 
|  | if (!url.is_valid()) { | 
|  | *error_out = "Invalid url_string."; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | GURL top_level_site = GURL(partition_key->top_level_site.value()); | 
|  | if (!top_level_site.is_valid()) { | 
|  | *error_out = "Invalid value for CookiePartitionKey.topLevelSite."; | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // has_cross_site_ancestor can not be false if url and top_level_site aren't | 
|  | // first party. | 
|  | if (!net::SiteForCookies::FromUrl(GURL(url)).IsFirstParty(top_level_site) && | 
|  | !partition_key->has_cross_site_ancestor.value()) { | 
|  | *error_out = | 
|  | "partitionKey has a first party value for hasCrossSiteAncestor " | 
|  | "when the url and the topLevelSite are not first party."; | 
|  | return false; | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | base::expected<std::optional<net::CookiePartitionKey>, std::string> | 
|  | ToNetCookiePartitionKey( | 
|  | const std::optional<extensions::api::cookies::CookiePartitionKey>& | 
|  | partition_key) { | 
|  | if (!partition_key.has_value()) { | 
|  | return std::nullopt; | 
|  | } | 
|  |  | 
|  | if (!partition_key->top_level_site.has_value()) { | 
|  | if (partition_key->has_cross_site_ancestor.has_value()) { | 
|  | return base::unexpected( | 
|  | "CookiePartitionKey.topLevelSite unexpectedly not present."); | 
|  | } | 
|  | return base::ok(std::nullopt); | 
|  | } | 
|  |  | 
|  | if (partition_key->top_level_site->empty()) { | 
|  | if (partition_key->has_cross_site_ancestor.has_value() && | 
|  | partition_key->has_cross_site_ancestor.value()) { | 
|  | return base::unexpected( | 
|  | "partitionKey with empty topLevelSite unexpectedly has a cross-site " | 
|  | "ancestor value of true."); | 
|  | } | 
|  | return base::ok(std::nullopt); | 
|  | } | 
|  |  | 
|  | base::expected<net::CookiePartitionKey, std::string> key = | 
|  | net::CookiePartitionKey::FromUntrustedInput( | 
|  | partition_key->top_level_site.value(), | 
|  | partition_key->has_cross_site_ancestor.value_or(true)); | 
|  | if (!key.has_value()) { | 
|  | return base::unexpected(key.error()); | 
|  | } | 
|  |  | 
|  | // 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 key; | 
|  | } | 
|  |  | 
|  | 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 (!net_partition_key.has_value()) { | 
|  | // Check to see if the api_partition_key is also unpartitioned. | 
|  | return !api_partition_key.has_value() || | 
|  | !api_partition_key->top_level_site.has_value() || | 
|  | api_partition_key.value().top_level_site.value().empty(); | 
|  | } | 
|  |  | 
|  | if (api_partition_key->has_cross_site_ancestor.has_value() && | 
|  | net_partition_key->IsThirdParty() != | 
|  | api_partition_key->has_cross_site_ancestor.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; | 
|  | } | 
|  |  | 
|  | 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(); | 
|  | } | 
|  |  | 
|  | if (!partition_key->has_cross_site_ancestor.has_value()) { | 
|  | return net::CookiePartitionKeyCollection::MatchesSite( | 
|  | net::SchemefulSite(GURL(partition_key->top_level_site.value()))); | 
|  | } | 
|  |  | 
|  | base::expected<net::CookiePartitionKey, std::string> net_partition_key = | 
|  | net::CookiePartitionKey::FromUntrustedInput( | 
|  | partition_key->top_level_site.value(), | 
|  | partition_key->has_cross_site_ancestor.value()); | 
|  | if (!net_partition_key.has_value()) { | 
|  | return net::CookiePartitionKeyCollection(); | 
|  | } | 
|  |  | 
|  | return net::CookiePartitionKeyCollection( | 
|  | std::move(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 |