| // 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. |
| |
| #include "net/cookies/cookie_util.h" |
| |
| #include <cstdio> |
| #include <cstdlib> |
| #include <string> |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_tokenizer.h" |
| #include "base/strings/string_util.h" |
| #include "base/types/optional_util.h" |
| #include "build/build_config.h" |
| #include "net/base/features.h" |
| #include "net/base/isolation_info.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| #include "net/base/url_util.h" |
| #include "net/cookies/cookie_access_delegate.h" |
| #include "net/cookies/cookie_constants.h" |
| #include "net/cookies/cookie_inclusion_status.h" |
| #include "net/cookies/cookie_monster.h" |
| #include "net/cookies/cookie_options.h" |
| #include "net/first_party_sets/first_party_set_metadata.h" |
| #include "net/first_party_sets/same_party_context.h" |
| #include "net/http/http_util.h" |
| #include "url/gurl.h" |
| #include "url/url_constants.h" |
| |
| namespace net::cookie_util { |
| |
| namespace { |
| |
| using ContextType = CookieOptions::SameSiteCookieContext::ContextType; |
| using ContextMetadata = CookieOptions::SameSiteCookieContext::ContextMetadata; |
| |
| base::Time MinNonNullTime() { |
| return base::Time::FromInternalValue(1); |
| } |
| |
| // Tries to assemble a base::Time given a base::Time::Exploded representing a |
| // UTC calendar date. |
| // |
| // If the date falls outside of the range supported internally by |
| // FromUTCExploded() on the current platform, then the result is: |
| // |
| // * Time(1) if it's below the range FromUTCExploded() supports. |
| // * Time::Max() if it's above the range FromUTCExploded() supports. |
| bool SaturatedTimeFromUTCExploded(const base::Time::Exploded& exploded, |
| base::Time* out) { |
| // Try to calculate the base::Time in the normal fashion. |
| if (base::Time::FromUTCExploded(exploded, out)) { |
| // Don't return Time(0) on success. |
| if (out->is_null()) |
| *out = MinNonNullTime(); |
| return true; |
| } |
| |
| // base::Time::FromUTCExploded() has platform-specific limits: |
| // |
| // * Windows: Years 1601 - 30827 |
| // * 32-bit POSIX: Years 1970 - 2038 |
| // |
| // Work around this by returning min/max valid times for times outside those |
| // ranges when imploding the time is doomed to fail. |
| // |
| // Note that the following implementation is NOT perfect. It will accept |
| // some invalid calendar dates in the out-of-range case. |
| if (!exploded.HasValidValues()) |
| return false; |
| |
| if (exploded.year > base::Time::kExplodedMaxYear) { |
| *out = base::Time::Max(); |
| return true; |
| } |
| if (exploded.year < base::Time::kExplodedMinYear) { |
| *out = MinNonNullTime(); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| struct ComputeSameSiteContextResult { |
| ContextType context_type = ContextType::CROSS_SITE; |
| ContextMetadata metadata; |
| }; |
| |
| CookieOptions::SameSiteCookieContext MakeSameSiteCookieContext( |
| const ComputeSameSiteContextResult& result, |
| const ComputeSameSiteContextResult& schemeful_result) { |
| return CookieOptions::SameSiteCookieContext( |
| result.context_type, schemeful_result.context_type, result.metadata, |
| schemeful_result.metadata); |
| } |
| |
| ContextMetadata::ContextRedirectTypeBug1221316 |
| ComputeContextRedirectTypeBug1221316(bool url_chain_is_length_one, |
| bool same_site_initiator, |
| bool site_for_cookies_is_same_site, |
| bool same_site_redirect_chain) { |
| if (url_chain_is_length_one) |
| return ContextMetadata::ContextRedirectTypeBug1221316::kNoRedirect; |
| |
| if (!same_site_initiator || !site_for_cookies_is_same_site) |
| return ContextMetadata::ContextRedirectTypeBug1221316::kCrossSiteRedirect; |
| |
| if (!same_site_redirect_chain) { |
| return ContextMetadata::ContextRedirectTypeBug1221316:: |
| kPartialSameSiteRedirect; |
| } |
| |
| return ContextMetadata::ContextRedirectTypeBug1221316::kAllSameSiteRedirect; |
| } |
| |
| // This function consolidates the common logic for computing SameSite cookie |
| // access context in various situations (HTTP vs JS; get vs set). |
| // |
| // `is_http` is whether the current cookie access request is associated with a |
| // network request (as opposed to a non-HTTP API, i.e., JavaScript). |
| // |
| // `compute_schemefully` is whether the current computation is for a |
| // schemeful_context, i.e. whether scheme should be considered when comparing |
| // two sites. |
| // |
| // See documentation of `ComputeSameSiteContextForRequest` for explanations of |
| // other parameters. |
| ComputeSameSiteContextResult ComputeSameSiteContext( |
| const std::vector<GURL>& url_chain, |
| const SiteForCookies& site_for_cookies, |
| const absl::optional<url::Origin>& initiator, |
| bool is_http, |
| bool is_main_frame_navigation, |
| bool compute_schemefully) { |
| DCHECK(!url_chain.empty()); |
| const GURL& request_url = url_chain.back(); |
| const auto is_same_site_with_site_for_cookies = |
| [&site_for_cookies, compute_schemefully](const GURL& url) { |
| return site_for_cookies.IsFirstPartyWithSchemefulMode( |
| url, compute_schemefully); |
| }; |
| |
| bool site_for_cookies_is_same_site = |
| is_same_site_with_site_for_cookies(request_url); |
| |
| // If the request is a main frame navigation, site_for_cookies must either be |
| // null (for opaque origins, e.g., data: origins) or same-site with the |
| // request URL (both schemefully and schemelessly), and the URL cannot be |
| // ws/wss (these schemes are not navigable). |
| DCHECK(!is_main_frame_navigation || site_for_cookies_is_same_site || |
| site_for_cookies.IsNull()); |
| DCHECK(!is_main_frame_navigation || !request_url.SchemeIsWSOrWSS()); |
| |
| // Defaults to a cross-site context type. |
| ComputeSameSiteContextResult result; |
| |
| // Create a SiteForCookies object from the initiator so that we can reuse |
| // IsFirstPartyWithSchemefulMode(). |
| bool same_site_initiator = |
| !initiator || |
| SiteForCookies::FromOrigin(initiator.value()) |
| .IsFirstPartyWithSchemefulMode(request_url, compute_schemefully); |
| |
| // Check that the URLs in the redirect chain are all same-site with the |
| // site_for_cookies and hence (by transitivity) same-site with the request |
| // URL. (If the URL chain only has one member, it's the request_url and we've |
| // already checked it previously.) |
| bool same_site_redirect_chain = |
| url_chain.size() == 1u || |
| base::ranges::all_of(url_chain, is_same_site_with_site_for_cookies); |
| |
| // Record what type of redirect was experienced. |
| |
| result.metadata.redirect_type_bug_1221316 = |
| ComputeContextRedirectTypeBug1221316( |
| url_chain.size() == 1u, same_site_initiator, |
| site_for_cookies_is_same_site, same_site_redirect_chain); |
| |
| if (!site_for_cookies_is_same_site) |
| return result; |
| |
| // Whether the context would be SAME_SITE_STRICT if not considering redirect |
| // chains, but is different after considering redirect chains. |
| bool cross_site_redirect_downgraded_from_strict = false; |
| // Allows the kCookieSameSiteConsidersRedirectChain feature to override the |
| // result and use SAME_SITE_STRICT. |
| bool use_strict = false; |
| |
| if (same_site_initiator) { |
| if (same_site_redirect_chain) { |
| result.context_type = ContextType::SAME_SITE_STRICT; |
| return result; |
| } |
| cross_site_redirect_downgraded_from_strict = true; |
| // If we are not supposed to consider redirect chains, record that the |
| // context result should ultimately be strictly same-site. We cannot |
| // just return early from here because we don't yet know what the context |
| // gets downgraded to, so we can't return with the correct metadata until we |
| // go through the rest of the logic below to determine that. |
| use_strict = !base::FeatureList::IsEnabled( |
| features::kCookieSameSiteConsidersRedirectChain); |
| } |
| |
| if (!is_http || is_main_frame_navigation) { |
| if (cross_site_redirect_downgraded_from_strict) { |
| result.metadata.cross_site_redirect_downgrade = |
| ContextMetadata::ContextDowngradeType::kStrictToLax; |
| } |
| result.context_type = |
| use_strict ? ContextType::SAME_SITE_STRICT : ContextType::SAME_SITE_LAX; |
| return result; |
| } |
| |
| if (cross_site_redirect_downgraded_from_strict) { |
| result.metadata.cross_site_redirect_downgrade = |
| ContextMetadata::ContextDowngradeType::kStrictToCross; |
| } |
| result.context_type = |
| use_strict ? ContextType::SAME_SITE_STRICT : ContextType::CROSS_SITE; |
| |
| return result; |
| } |
| |
| // Setting any SameSite={Strict,Lax} cookie only requires a LAX context, so |
| // normalize any strictly same-site contexts to Lax for cookie writes. |
| void NormalizeStrictToLaxForSet(ComputeSameSiteContextResult& result) { |
| if (result.context_type == ContextType::SAME_SITE_STRICT) |
| result.context_type = ContextType::SAME_SITE_LAX; |
| |
| switch (result.metadata.cross_site_redirect_downgrade) { |
| case ContextMetadata::ContextDowngradeType::kStrictToLax: |
| result.metadata.cross_site_redirect_downgrade = |
| ContextMetadata::ContextDowngradeType::kNoDowngrade; |
| break; |
| case ContextMetadata::ContextDowngradeType::kStrictToCross: |
| result.metadata.cross_site_redirect_downgrade = |
| ContextMetadata::ContextDowngradeType::kLaxToCross; |
| break; |
| default: |
| break; |
| } |
| } |
| |
| CookieOptions::SameSiteCookieContext ComputeSameSiteContextForSet( |
| const std::vector<GURL>& url_chain, |
| const SiteForCookies& site_for_cookies, |
| const absl::optional<url::Origin>& initiator, |
| bool is_http, |
| bool is_main_frame_navigation) { |
| CookieOptions::SameSiteCookieContext same_site_context; |
| |
| ComputeSameSiteContextResult result = ComputeSameSiteContext( |
| url_chain, site_for_cookies, initiator, is_http, is_main_frame_navigation, |
| false /* compute_schemefully */); |
| ComputeSameSiteContextResult schemeful_result = ComputeSameSiteContext( |
| url_chain, site_for_cookies, initiator, is_http, is_main_frame_navigation, |
| true /* compute_schemefully */); |
| |
| NormalizeStrictToLaxForSet(result); |
| NormalizeStrictToLaxForSet(schemeful_result); |
| |
| return MakeSameSiteCookieContext(result, schemeful_result); |
| } |
| |
| bool CookieWithAccessResultSorter(const CookieWithAccessResult& a, |
| const CookieWithAccessResult& b) { |
| return CookieMonster::CookieSorter(&a.cookie, &b.cookie); |
| } |
| |
| } // namespace |
| |
| void FireStorageAccessHistogram(StorageAccessResult result) { |
| UMA_HISTOGRAM_ENUMERATION("API.StorageAccess.AllowedRequests2", result); |
| } |
| |
| bool DomainIsHostOnly(const std::string& domain_string) { |
| return (domain_string.empty() || domain_string[0] != '.'); |
| } |
| |
| std::string CookieDomainAsHost(const std::string& cookie_domain) { |
| if (DomainIsHostOnly(cookie_domain)) |
| return cookie_domain; |
| return cookie_domain.substr(1); |
| } |
| |
| std::string GetEffectiveDomain(const std::string& scheme, |
| const std::string& host) { |
| if (scheme == "http" || scheme == "https" || scheme == "ws" || |
| scheme == "wss") { |
| return registry_controlled_domains::GetDomainAndRegistry( |
| host, |
| registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); |
| } |
| |
| return CookieDomainAsHost(host); |
| } |
| |
| bool GetCookieDomainWithString(const GURL& url, |
| const std::string& domain_string, |
| CookieInclusionStatus& status, |
| std::string* result) { |
| // Disallow non-ASCII domain names. |
| if (!base::IsStringASCII(domain_string)) { |
| if (base::FeatureList::IsEnabled(features::kCookieDomainRejectNonASCII)) { |
| status.AddExclusionReason( |
| CookieInclusionStatus::EXCLUDE_DOMAIN_NON_ASCII); |
| return false; |
| } |
| status.AddWarningReason(CookieInclusionStatus::WARN_DOMAIN_NON_ASCII); |
| } |
| |
| const std::string url_host(url.host()); |
| |
| // Disallow invalid hostnames containing multiple `.` at the end. |
| // Httpbis-rfc6265bis draft-11, §5.1.2 says to convert the request host "into |
| // a sequence of individual domain name labels"; a label can only be empty if |
| // it is the last label in the name, but a name ending in `..` would have an |
| // empty label in the penultimate position and is thus invalid. |
| /* Cobalt |
| if (url_host.ends_with("..")) { |
| Cobalt */ |
| if (base::EndsWith(url_host, "..")) { |
| return false; |
| } |
| // If no domain was specified in the domain string, default to a host cookie. |
| // We match IE/Firefox in allowing a domain=IPADDR if it matches (case |
| // in-sensitive) the url ip address hostname and ignoring a leading dot if one |
| // exists. It should be treated as a host cookie. |
| if (domain_string.empty() || |
| (url.HostIsIPAddress() && |
| (base::EqualsCaseInsensitiveASCII(url_host, domain_string) || |
| base::EqualsCaseInsensitiveASCII("." + url_host, domain_string)))) { |
| *result = url_host; |
| DCHECK(DomainIsHostOnly(*result)); |
| return true; |
| } |
| |
| // Disallow domain names with %-escaped characters. |
| for (char c : domain_string) { |
| if (c == '%') |
| return false; |
| } |
| |
| url::CanonHostInfo ignored; |
| std::string cookie_domain(CanonicalizeHost(domain_string, &ignored)); |
| // Get the normalized domain specified in cookie line. |
| if (cookie_domain.empty()) |
| return false; |
| if (cookie_domain[0] != '.') |
| cookie_domain = "." + cookie_domain; |
| |
| // Ensure |url| and |cookie_domain| have the same domain+registry. |
| const std::string url_scheme(url.scheme()); |
| const std::string url_domain_and_registry( |
| GetEffectiveDomain(url_scheme, url_host)); |
| if (url_domain_and_registry.empty()) { |
| // We match IE/Firefox by treating an exact match between the normalized |
| // domain attribute and the request host to be treated as a host cookie. |
| std::string normalized_domain_string = base::ToLowerASCII( |
| domain_string[0] == '.' ? domain_string.substr(1) : domain_string); |
| |
| if (url_host == normalized_domain_string) { |
| *result = url_host; |
| DCHECK(DomainIsHostOnly(*result)); |
| return true; |
| } |
| |
| // Otherwise, IP addresses/intranet hosts/public suffixes can't set |
| // domain cookies. |
| return false; |
| } |
| const std::string cookie_domain_and_registry( |
| GetEffectiveDomain(url_scheme, cookie_domain)); |
| if (url_domain_and_registry != cookie_domain_and_registry) |
| return false; // Can't set a cookie on a different domain + registry. |
| |
| // Ensure |url_host| is |cookie_domain| or one of its subdomains. Given that |
| // we know the domain+registry are the same from the above checks, this is |
| // basically a simple string suffix check. |
| const bool is_suffix = (url_host.length() < cookie_domain.length()) ? |
| (cookie_domain != ("." + url_host)) : |
| (url_host.compare(url_host.length() - cookie_domain.length(), |
| cookie_domain.length(), cookie_domain) != 0); |
| if (is_suffix) |
| return false; |
| |
| *result = cookie_domain; |
| return true; |
| } |
| |
| // Parse a cookie expiration time. We try to be lenient, but we need to |
| // assume some order to distinguish the fields. The basic rules: |
| // - The month name must be present and prefix the first 3 letters of the |
| // full month name (jan for January, jun for June). |
| // - If the year is <= 2 digits, it must occur after the day of month. |
| // - The time must be of the format hh:mm:ss. |
| // An average cookie expiration will look something like this: |
| // Sat, 15-Apr-17 21:01:22 GMT |
| base::Time ParseCookieExpirationTime(const std::string& time_string) { |
| static const char* const kMonths[] = { |
| "jan", "feb", "mar", "apr", "may", "jun", |
| "jul", "aug", "sep", "oct", "nov", "dec" }; |
| // We want to be pretty liberal, and support most non-ascii and non-digit |
| // characters as a delimiter. We can't treat : as a delimiter, because it |
| // is the delimiter for hh:mm:ss, and we want to keep this field together. |
| // We make sure to include - and +, since they could prefix numbers. |
| // If the cookie attribute came in in quotes (ex expires="XXX"), the quotes |
| // will be preserved, and we will get them here. So we make sure to include |
| // quote characters, and also \ for anything that was internally escaped. |
| static const char kDelimiters[] = "\t !\"#$%&'()*+,-./;<=>?@[\\]^_`{|}~"; |
| |
| base::Time::Exploded exploded = {0}; |
| |
| base::StringTokenizer tokenizer(time_string, kDelimiters); |
| |
| bool found_day_of_month = false; |
| bool found_month = false; |
| bool found_time = false; |
| bool found_year = false; |
| |
| while (tokenizer.GetNext()) { |
| const std::string token = tokenizer.token(); |
| DCHECK(!token.empty()); |
| bool numerical = base::IsAsciiDigit(token[0]); |
| |
| // String field |
| if (!numerical) { |
| if (!found_month) { |
| for (size_t i = 0; i < std::size(kMonths); ++i) { |
| // Match prefix, so we could match January, etc |
| if (base::StartsWith(token, base::StringPiece(kMonths[i], 3), |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| exploded.month = static_cast<int>(i) + 1; |
| found_month = true; |
| break; |
| } |
| } |
| } else { |
| // If we've gotten here, it means we've already found and parsed our |
| // month, and we have another string, which we would expect to be the |
| // the time zone name. According to the RFC and my experiments with |
| // how sites format their expirations, we don't have much of a reason |
| // to support timezones. We don't want to ever barf on user input, |
| // but this DCHECK should pass for well-formed data. |
| // DCHECK(token == "GMT"); |
| } |
| // Numeric field w/ a colon |
| } else if (token.find(':') != std::string::npos) { |
| if (!found_time && |
| #ifdef COMPILER_MSVC |
| sscanf_s( |
| #else |
| sscanf( |
| #endif |
| token.c_str(), "%2u:%2u:%2u", &exploded.hour, |
| &exploded.minute, &exploded.second) == 3) { |
| found_time = true; |
| } else { |
| // We should only ever encounter one time-like thing. If we're here, |
| // it means we've found a second, which shouldn't happen. We keep |
| // the first. This check should be ok for well-formed input: |
| // NOTREACHED(); |
| } |
| // Numeric field |
| } else { |
| // Overflow with atoi() is unspecified, so we enforce a max length. |
| if (!found_day_of_month && token.length() <= 2) { |
| exploded.day_of_month = atoi(token.c_str()); |
| found_day_of_month = true; |
| } else if (!found_year && token.length() <= 5) { |
| exploded.year = atoi(token.c_str()); |
| found_year = true; |
| } else { |
| // If we're here, it means we've either found an extra numeric field, |
| // or a numeric field which was too long. For well-formed input, the |
| // following check would be reasonable: |
| // NOTREACHED(); |
| } |
| } |
| } |
| |
| if (!found_day_of_month || !found_month || !found_time || !found_year) { |
| // We didn't find all of the fields we need. For well-formed input, the |
| // following check would be reasonable: |
| // NOTREACHED() << "Cookie parse expiration failed: " << time_string; |
| return base::Time(); |
| } |
| |
| // Normalize the year to expand abbreviated years to the full year. |
| if (exploded.year >= 70 && exploded.year <= 99) |
| exploded.year += 1900; |
| if (exploded.year >= 0 && exploded.year <= 69) |
| exploded.year += 2000; |
| |
| // Note that clipping the date if it is outside of a platform-specific range |
| // is permitted by: https://tools.ietf.org/html/rfc6265#section-5.2.1 |
| base::Time result; |
| if (SaturatedTimeFromUTCExploded(exploded, &result)) |
| return result; |
| |
| // One of our values was out of expected range. For well-formed input, |
| // the following check would be reasonable: |
| // NOTREACHED() << "Cookie exploded expiration failed: " << time_string; |
| |
| return base::Time(); |
| } |
| |
| GURL CookieDomainAndPathToURL(const std::string& domain, |
| const std::string& path, |
| const std::string& source_scheme) { |
| // Note: domain_no_dot could be empty for e.g. file cookies. |
| std::string domain_no_dot = CookieDomainAsHost(domain); |
| if (domain_no_dot.empty() || source_scheme.empty()) |
| return GURL(); |
| return GURL(base::StrCat( |
| {source_scheme, url::kStandardSchemeSeparator, domain_no_dot, path})); |
| } |
| |
| GURL CookieDomainAndPathToURL(const std::string& domain, |
| const std::string& path, |
| bool is_https) { |
| return CookieDomainAndPathToURL( |
| domain, path, |
| std::string(is_https ? url::kHttpsScheme : url::kHttpScheme)); |
| } |
| |
| GURL CookieDomainAndPathToURL(const std::string& domain, |
| const std::string& path, |
| CookieSourceScheme source_scheme) { |
| return CookieDomainAndPathToURL(domain, path, |
| source_scheme == CookieSourceScheme::kSecure); |
| } |
| |
| GURL CookieOriginToURL(const std::string& domain, bool is_https) { |
| return CookieDomainAndPathToURL(domain, "/", is_https); |
| } |
| |
| GURL SimulatedCookieSource(const CanonicalCookie& cookie, |
| const std::string& source_scheme) { |
| return CookieDomainAndPathToURL(cookie.Domain(), cookie.Path(), |
| source_scheme); |
| } |
| |
| CookieAccessScheme ProvisionalAccessScheme(const GURL& source_url) { |
| return source_url.SchemeIsCryptographic() |
| ? CookieAccessScheme::kCryptographic |
| : IsLocalhost(source_url) ? CookieAccessScheme::kTrustworthy |
| : CookieAccessScheme::kNonCryptographic; |
| } |
| |
| bool IsDomainMatch(const std::string& domain, const std::string& host) { |
| // Can domain match in two ways; as a domain cookie (where the cookie |
| // domain begins with ".") or as a host cookie (where it doesn't). |
| |
| // Some consumers of the CookieMonster expect to set cookies on |
| // URLs like http://.strange.url. To retrieve cookies in this instance, |
| // we allow matching as a host cookie even when the domain_ starts with |
| // a period. |
| if (host == domain) |
| return true; |
| |
| // Domain cookie must have an initial ".". To match, it must be |
| // equal to url's host with initial period removed, or a suffix of |
| // it. |
| |
| // Arguably this should only apply to "http" or "https" cookies, but |
| // extension cookie tests currently use the funtionality, and if we |
| // ever decide to implement that it should be done by preventing |
| // such cookies from being set. |
| if (domain.empty() || domain[0] != '.') |
| return false; |
| |
| // The host with a "." prefixed. |
| if (domain.compare(1, std::string::npos, host) == 0) |
| return true; |
| |
| // A pure suffix of the host (ok since we know the domain already |
| // starts with a ".") |
| return (host.length() > domain.length() && |
| host.compare(host.length() - domain.length(), domain.length(), |
| domain) == 0); |
| } |
| |
| bool IsOnPath(const std::string& cookie_path, const std::string& url_path) { |
| // A zero length would be unsafe for our trailing '/' checks, and |
| // would also make no sense for our prefix match. The code that |
| // creates a CanonicalCookie should make sure the path is never zero length, |
| // but we double check anyway. |
| if (cookie_path.empty()) { |
| return false; |
| } |
| |
| // The Mozilla code broke this into three cases, based on if the cookie path |
| // was longer, the same length, or shorter than the length of the url path. |
| // I think the approach below is simpler. |
| |
| // Make sure the cookie path is a prefix of the url path. If the url path is |
| // shorter than the cookie path, then the cookie path can't be a prefix. |
| if (!base::StartsWith(url_path, cookie_path, base::CompareCase::SENSITIVE)) { |
| return false; |
| } |
| |
| // |url_path| is >= |cookie_path|, and |cookie_path| is a prefix of |
| // |url_path|. If they are the are the same length then they are identical, |
| // otherwise need an additional check: |
| |
| // In order to avoid in correctly matching a cookie path of /blah |
| // with a request path of '/blahblah/', we need to make sure that either |
| // the cookie path ends in a trailing '/', or that we prefix up to a '/' |
| // in the url path. Since we know that the url path length is greater |
| // than the cookie path length, it's safe to index one byte past. |
| if (cookie_path.length() != url_path.length() && cookie_path.back() != '/' && |
| url_path[cookie_path.length()] != '/') { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void ParseRequestCookieLine(const std::string& header_value, |
| ParsedRequestCookies* parsed_cookies) { |
| std::string::const_iterator i = header_value.begin(); |
| while (i != header_value.end()) { |
| // Here we are at the beginning of a cookie. |
| |
| // Eat whitespace. |
| while (i != header_value.end() && *i == ' ') ++i; |
| if (i == header_value.end()) return; |
| |
| // Find cookie name. |
| std::string::const_iterator cookie_name_beginning = i; |
| while (i != header_value.end() && *i != '=') ++i; |
| auto cookie_name = base::MakeStringPiece(cookie_name_beginning, i); |
| |
| // Find cookie value. |
| base::StringPiece cookie_value; |
| // Cookies may have no value, in this case '=' may or may not be there. |
| if (i != header_value.end() && i + 1 != header_value.end()) { |
| ++i; // Skip '='. |
| std::string::const_iterator cookie_value_beginning = i; |
| if (*i == '"') { |
| ++i; // Skip '"'. |
| while (i != header_value.end() && *i != '"') ++i; |
| if (i == header_value.end()) return; |
| ++i; // Skip '"'. |
| cookie_value = base::MakeStringPiece(cookie_value_beginning, i); |
| // i points to character after '"', potentially a ';'. |
| } else { |
| while (i != header_value.end() && *i != ';') ++i; |
| cookie_value = base::MakeStringPiece(cookie_value_beginning, i); |
| // i points to ';' or end of string. |
| } |
| } |
| parsed_cookies->emplace_back(std::string(cookie_name), |
| std::string(cookie_value)); |
| // Eat ';'. |
| if (i != header_value.end()) ++i; |
| } |
| } |
| |
| std::string SerializeRequestCookieLine( |
| const ParsedRequestCookies& parsed_cookies) { |
| std::string buffer; |
| for (const auto& parsed_cookie : parsed_cookies) { |
| if (!buffer.empty()) |
| buffer.append("; "); |
| buffer.append(parsed_cookie.first.begin(), parsed_cookie.first.end()); |
| buffer.push_back('='); |
| buffer.append(parsed_cookie.second.begin(), parsed_cookie.second.end()); |
| } |
| return buffer; |
| } |
| |
| CookieOptions::SameSiteCookieContext ComputeSameSiteContextForRequest( |
| const std::string& http_method, |
| const std::vector<GURL>& url_chain, |
| const SiteForCookies& site_for_cookies, |
| const absl::optional<url::Origin>& initiator, |
| bool is_main_frame_navigation, |
| bool force_ignore_site_for_cookies) { |
| // Set SameSiteCookieContext according to the rules laid out in |
| // https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis: |
| // |
| // * Include both "strict" and "lax" same-site cookies if the request's |
| // |url|, |initiator|, and |site_for_cookies| all have the same |
| // registrable domain. Note: this also covers the case of a request |
| // without an initiator (only happens for browser-initiated main frame |
| // navigations). If computing schemefully, the schemes must also match. |
| // |
| // * Include only "lax" same-site cookies if the request's |URL| and |
| // |site_for_cookies| have the same registrable domain, _and_ the |
| // request's |http_method| is "safe" ("GET" or "HEAD"), and the request |
| // is a main frame navigation. |
| // |
| // This case should occur only for cross-site requests which |
| // target a top-level browsing context, with a "safe" method. |
| // |
| // * Include both "strict" and "lax" same-site cookies if the request is |
| // tagged with a flag allowing it. |
| // |
| // Note that this can be the case for requests initiated by extensions, |
| // which need to behave as though they are made by the document itself, |
| // but appear like cross-site ones. |
| // |
| // * Otherwise, do not include same-site cookies. |
| |
| if (force_ignore_site_for_cookies) |
| return CookieOptions::SameSiteCookieContext::MakeInclusive(); |
| |
| ComputeSameSiteContextResult result = ComputeSameSiteContext( |
| url_chain, site_for_cookies, initiator, true /* is_http */, |
| is_main_frame_navigation, false /* compute_schemefully */); |
| ComputeSameSiteContextResult schemeful_result = ComputeSameSiteContext( |
| url_chain, site_for_cookies, initiator, true /* is_http */, |
| is_main_frame_navigation, true /* compute_schemefully */); |
| |
| // If the method is safe, the context is Lax. Otherwise, make a note that |
| // the method is unsafe. |
| if (!net::HttpUtil::IsMethodSafe(http_method)) { |
| if (result.context_type == ContextType::SAME_SITE_LAX) |
| result.context_type = ContextType::SAME_SITE_LAX_METHOD_UNSAFE; |
| if (schemeful_result.context_type == ContextType::SAME_SITE_LAX) |
| schemeful_result.context_type = ContextType::SAME_SITE_LAX_METHOD_UNSAFE; |
| } |
| |
| ContextMetadata::HttpMethod http_method_enum = |
| HttpMethodStringToEnum(http_method); |
| |
| if (result.metadata.cross_site_redirect_downgrade != |
| ContextMetadata::ContextDowngradeType::kNoDowngrade) { |
| result.metadata.http_method_bug_1221316 = http_method_enum; |
| } |
| |
| if (schemeful_result.metadata.cross_site_redirect_downgrade != |
| ContextMetadata::ContextDowngradeType::kNoDowngrade) { |
| schemeful_result.metadata.http_method_bug_1221316 = http_method_enum; |
| } |
| |
| return MakeSameSiteCookieContext(result, schemeful_result); |
| } |
| |
| NET_EXPORT CookieOptions::SameSiteCookieContext |
| ComputeSameSiteContextForScriptGet(const GURL& url, |
| const SiteForCookies& site_for_cookies, |
| const absl::optional<url::Origin>& initiator, |
| bool force_ignore_site_for_cookies) { |
| if (force_ignore_site_for_cookies) |
| return CookieOptions::SameSiteCookieContext::MakeInclusive(); |
| |
| // We don't check the redirect chain for script access to cookies (only the |
| // URL itself). |
| ComputeSameSiteContextResult result = ComputeSameSiteContext( |
| {url}, site_for_cookies, initiator, false /* is_http */, |
| false /* is_main_frame_navigation */, false /* compute_schemefully */); |
| ComputeSameSiteContextResult schemeful_result = ComputeSameSiteContext( |
| {url}, site_for_cookies, initiator, false /* is_http */, |
| false /* is_main_frame_navigation */, true /* compute_schemefully */); |
| |
| return MakeSameSiteCookieContext(result, schemeful_result); |
| } |
| |
| CookieOptions::SameSiteCookieContext ComputeSameSiteContextForResponse( |
| const std::vector<GURL>& url_chain, |
| const SiteForCookies& site_for_cookies, |
| const absl::optional<url::Origin>& initiator, |
| bool is_main_frame_navigation, |
| bool force_ignore_site_for_cookies) { |
| if (force_ignore_site_for_cookies) |
| return CookieOptions::SameSiteCookieContext::MakeInclusiveForSet(); |
| |
| DCHECK(!url_chain.empty()); |
| if (is_main_frame_navigation && !site_for_cookies.IsNull()) { |
| // If the request is a main frame navigation, site_for_cookies must either |
| // be null (for opaque origins, e.g., data: origins) or same-site with the |
| // request URL (both schemefully and schemelessly), and the URL cannot be |
| // ws/wss (these schemes are not navigable). |
| DCHECK( |
| site_for_cookies.IsFirstPartyWithSchemefulMode(url_chain.back(), true)); |
| DCHECK(!url_chain.back().SchemeIsWSOrWSS()); |
| CookieOptions::SameSiteCookieContext result = |
| CookieOptions::SameSiteCookieContext::MakeInclusiveForSet(); |
| |
| const GURL& request_url = url_chain.back(); |
| |
| for (bool compute_schemefully : {false, true}) { |
| bool same_site_initiator = |
| !initiator || |
| SiteForCookies::FromOrigin(initiator.value()) |
| .IsFirstPartyWithSchemefulMode(request_url, compute_schemefully); |
| |
| const auto is_same_site_with_site_for_cookies = |
| [&site_for_cookies, compute_schemefully](const GURL& url) { |
| return site_for_cookies.IsFirstPartyWithSchemefulMode( |
| url, compute_schemefully); |
| }; |
| |
| bool same_site_redirect_chain = |
| url_chain.size() == 1u || |
| base::ranges::all_of(url_chain, is_same_site_with_site_for_cookies); |
| |
| CookieOptions::SameSiteCookieContext::ContextMetadata& result_metadata = |
| compute_schemefully ? result.schemeful_metadata() : result.metadata(); |
| |
| result_metadata.redirect_type_bug_1221316 = |
| ComputeContextRedirectTypeBug1221316( |
| url_chain.size() == 1u, same_site_initiator, |
| true /* site_for_cookies_is_same_site */, |
| same_site_redirect_chain); |
| } |
| return result; |
| } |
| |
| return ComputeSameSiteContextForSet(url_chain, site_for_cookies, initiator, |
| true /* is_http */, |
| is_main_frame_navigation); |
| } |
| |
| CookieOptions::SameSiteCookieContext ComputeSameSiteContextForScriptSet( |
| const GURL& url, |
| const SiteForCookies& site_for_cookies, |
| bool force_ignore_site_for_cookies) { |
| if (force_ignore_site_for_cookies) |
| return CookieOptions::SameSiteCookieContext::MakeInclusiveForSet(); |
| |
| // It doesn't matter what initiator origin we pass here. Either way, the |
| // context will be considered same-site iff the site_for_cookies is same-site |
| // with the url. We don't check the redirect chain for script access to |
| // cookies (only the URL itself). |
| return ComputeSameSiteContextForSet( |
| {url}, site_for_cookies, absl::nullopt /* initiator */, |
| false /* is_http */, false /* is_main_frame_navigation */); |
| } |
| |
| CookieOptions::SameSiteCookieContext ComputeSameSiteContextForSubresource( |
| const GURL& url, |
| const SiteForCookies& site_for_cookies, |
| bool force_ignore_site_for_cookies) { |
| if (force_ignore_site_for_cookies) |
| return CookieOptions::SameSiteCookieContext::MakeInclusive(); |
| |
| // If the URL is same-site as site_for_cookies it's same-site as all frames |
| // in the tree from the initiator frame up --- including the initiator frame. |
| |
| // Schemeless check |
| if (!site_for_cookies.IsFirstPartyWithSchemefulMode(url, false)) { |
| return CookieOptions::SameSiteCookieContext(ContextType::CROSS_SITE, |
| ContextType::CROSS_SITE); |
| } |
| |
| // Schemeful check |
| if (!site_for_cookies.IsFirstPartyWithSchemefulMode(url, true)) { |
| return CookieOptions::SameSiteCookieContext(ContextType::SAME_SITE_STRICT, |
| ContextType::CROSS_SITE); |
| } |
| |
| return CookieOptions::SameSiteCookieContext::MakeInclusive(); |
| } |
| |
| bool IsSchemefulSameSiteEnabled() { |
| return base::FeatureList::IsEnabled(features::kSchemefulSameSite); |
| } |
| |
| absl::optional<FirstPartySetMetadata> ComputeFirstPartySetMetadataMaybeAsync( |
| const SchemefulSite& request_site, |
| const IsolationInfo& isolation_info, |
| const CookieAccessDelegate* cookie_access_delegate, |
| bool force_ignore_top_frame_party, |
| base::OnceCallback<void(FirstPartySetMetadata)> callback) { |
| if (!isolation_info.IsEmpty() && isolation_info.party_context().has_value() && |
| cookie_access_delegate) { |
| return cookie_access_delegate->ComputeFirstPartySetMetadataMaybeAsync( |
| request_site, |
| force_ignore_top_frame_party |
| ? nullptr |
| : base::OptionalToPtr( |
| isolation_info.network_isolation_key().GetTopFrameSite()), |
| isolation_info.party_context().value(), std::move(callback)); |
| } |
| |
| return FirstPartySetMetadata(); |
| } |
| |
| CookieOptions::SameSiteCookieContext::ContextMetadata::HttpMethod |
| HttpMethodStringToEnum(const std::string& in) { |
| using HttpMethod = |
| CookieOptions::SameSiteCookieContext::ContextMetadata::HttpMethod; |
| if (in == "GET") |
| return HttpMethod::kGet; |
| if (in == "HEAD") |
| return HttpMethod::kHead; |
| if (in == "POST") |
| return HttpMethod::kPost; |
| if (in == "PUT") |
| return HttpMethod::KPut; |
| if (in == "DELETE") |
| return HttpMethod::kDelete; |
| if (in == "CONNECT") |
| return HttpMethod::kConnect; |
| if (in == "OPTIONS") |
| return HttpMethod::kOptions; |
| if (in == "TRACE") |
| return HttpMethod::kTrace; |
| if (in == "PATCH") |
| return HttpMethod::kPatch; |
| |
| return HttpMethod::kUnknown; |
| } |
| |
| CookieSamePartyStatus GetSamePartyStatus( |
| const CanonicalCookie& cookie, |
| const CookieOptions& options, |
| const bool same_party_attribute_enabled) { |
| if (!same_party_attribute_enabled || !cookie.IsSameParty() || |
| !options.is_in_nontrivial_first_party_set()) { |
| return CookieSamePartyStatus::kNoSamePartyEnforcement; |
| } |
| |
| switch (options.same_party_context().context_type()) { |
| case SamePartyContext::Type::kCrossParty: |
| return CookieSamePartyStatus::kEnforceSamePartyExclude; |
| case SamePartyContext::Type::kSameParty: |
| return CookieSamePartyStatus::kEnforceSamePartyInclude; |
| }; |
| } |
| |
| bool IsCookieAccessResultInclude(CookieAccessResult cookie_access_result) { |
| return cookie_access_result.status.IsInclude(); |
| } |
| |
| CookieList StripAccessResults( |
| const CookieAccessResultList& cookie_access_results_list) { |
| CookieList cookies; |
| for (const CookieWithAccessResult& cookie_with_access_result : |
| cookie_access_results_list) { |
| cookies.push_back(cookie_with_access_result.cookie); |
| } |
| return cookies; |
| } |
| |
| NET_EXPORT void RecordCookiePortOmniboxHistograms(const GURL& url) { |
| int port = url.EffectiveIntPort(); |
| |
| if (port == url::PORT_UNSPECIFIED) |
| return; |
| |
| if (IsLocalhost(url)) { |
| UMA_HISTOGRAM_ENUMERATION("Cookie.Port.OmniboxURLNavigation.Localhost", |
| ReducePortRangeForCookieHistogram(port)); |
| } else { |
| UMA_HISTOGRAM_ENUMERATION("Cookie.Port.OmniboxURLNavigation.RemoteHost", |
| ReducePortRangeForCookieHistogram(port)); |
| } |
| } |
| |
| NET_EXPORT void DCheckIncludedAndExcludedCookieLists( |
| const CookieAccessResultList& included_cookies, |
| const CookieAccessResultList& excluded_cookies) { |
| // Check that all elements of `included_cookies` really should be included, |
| // and that all elements of `excluded_cookies` really should be excluded. |
| DCHECK(base::ranges::all_of(included_cookies, |
| [](const net::CookieWithAccessResult& cookie) { |
| return cookie.access_result.status.IsInclude(); |
| })); |
| DCHECK(base::ranges::none_of(excluded_cookies, |
| [](const net::CookieWithAccessResult& cookie) { |
| return cookie.access_result.status.IsInclude(); |
| })); |
| |
| // Check that the included cookies are still in the correct order. |
| DCHECK( |
| base::ranges::is_sorted(included_cookies, CookieWithAccessResultSorter)); |
| } |
| |
| } // namespace net::cookie_util |