| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/network/restricted_cookie_manager.h" |
| |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/compiler_specific.h" // for FALLTHROUGH; |
| #include "base/debug/crash_logging.h" |
| #include "base/debug/dump_without_crashing.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/strings/string_util.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "mojo/public/cpp/bindings/message.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| #include "net/cookies/cookie_constants.h" |
| #include "net/cookies/cookie_options.h" |
| #include "net/cookies/cookie_store.h" |
| #include "net/cookies/cookie_util.h" |
| #include "services/network/cookie_settings.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| #include "services/network/public/mojom/network_service.mojom.h" |
| #include "url/gurl.h" |
| |
| namespace network { |
| |
| namespace { |
| |
| net::CookieOptions MakeOptionsForSet(mojom::RestrictedCookieManagerRole role, |
| const GURL& url, |
| const GURL& site_for_cookies, |
| const CookieSettings* cookie_settings) { |
| net::CookieOptions options; |
| bool attach_same_site_cookies = |
| cookie_settings->ShouldIgnoreSameSiteRestrictions(url, site_for_cookies); |
| if (role == mojom::RestrictedCookieManagerRole::SCRIPT) { |
| options.set_exclude_httponly(); // Default, but make it explicit here. |
| options.set_same_site_cookie_context( |
| net::cookie_util::ComputeSameSiteContextForScriptSet( |
| url, site_for_cookies, attach_same_site_cookies)); |
| } else { |
| // mojom::RestrictedCookieManagerRole::NETWORK |
| options.set_include_httponly(); |
| options.set_same_site_cookie_context( |
| net::cookie_util::ComputeSameSiteContextForSubresource( |
| url, site_for_cookies, attach_same_site_cookies)); |
| } |
| return options; |
| } |
| |
| net::CookieOptions MakeOptionsForGet(mojom::RestrictedCookieManagerRole role, |
| const GURL& url, |
| const GURL& site_for_cookies, |
| const CookieSettings* cookie_settings) { |
| // TODO(https://crbug.com/925311): Wire initiator here. |
| net::CookieOptions options; |
| bool attach_same_site_cookies = |
| cookie_settings->ShouldIgnoreSameSiteRestrictions(url, site_for_cookies); |
| if (role == mojom::RestrictedCookieManagerRole::SCRIPT) { |
| options.set_exclude_httponly(); // Default, but make it explicit here. |
| options.set_same_site_cookie_context( |
| net::cookie_util::ComputeSameSiteContextForScriptGet( |
| url, site_for_cookies, base::nullopt /*initiator*/, |
| attach_same_site_cookies)); |
| } else { |
| // mojom::RestrictedCookieManagerRole::NETWORK |
| options.set_include_httponly(); |
| options.set_same_site_cookie_context( |
| net::cookie_util::ComputeSameSiteContextForSubresource( |
| url, site_for_cookies, attach_same_site_cookies)); |
| } |
| return options; |
| } |
| |
| } // namespace |
| |
| using CookieInclusionStatus = net::CanonicalCookie::CookieInclusionStatus; |
| |
| class RestrictedCookieManager::Listener : public base::LinkNode<Listener> { |
| public: |
| Listener(net::CookieStore* cookie_store, |
| const RestrictedCookieManager* restricted_cookie_manager, |
| const GURL& url, |
| const GURL& site_for_cookies, |
| const url::Origin& top_frame_origin, |
| net::CookieOptions options, |
| mojo::PendingRemote<mojom::CookieChangeListener> mojo_listener) |
| : restricted_cookie_manager_(restricted_cookie_manager), |
| url_(url), |
| site_for_cookies_(site_for_cookies), |
| top_frame_origin_(top_frame_origin), |
| options_(options), |
| mojo_listener_(std::move(mojo_listener)) { |
| // TODO(pwnall): add a constructor w/options to net::CookieChangeDispatcher. |
| cookie_store_subscription_ = |
| cookie_store->GetChangeDispatcher().AddCallbackForUrl( |
| url, |
| base::BindRepeating( |
| &Listener::OnCookieChange, |
| // Safe because net::CookieChangeDispatcher guarantees that the |
| // callback will stop being called immediately after we remove |
| // the subscription, and the cookie store lives on the same |
| // thread as we do. |
| base::Unretained(this))); |
| } |
| |
| ~Listener() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); } |
| |
| mojo::Remote<mojom::CookieChangeListener>& mojo_listener() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return mojo_listener_; |
| } |
| |
| private: |
| // net::CookieChangeDispatcher callback. |
| void OnCookieChange(const net::CookieChangeInfo& change) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!change.cookie |
| .IncludeForRequestURL(url_, options_, change.access_semantics) |
| .IsInclude()) { |
| return; |
| } |
| |
| // When a user blocks a site's access to cookies, the existing cookies are |
| // not deleted. This check prevents the site from observing their cookies |
| // being deleted at a later time, which can happen due to eviction or due to |
| // the user explicitly deleting all cookies. |
| if (!restricted_cookie_manager_->cookie_settings()->IsCookieAccessAllowed( |
| url_, site_for_cookies_, top_frame_origin_)) { |
| return; |
| } |
| |
| mojo_listener_->OnCookieChange(change); |
| } |
| |
| // The CookieChangeDispatcher subscription used by this listener. |
| std::unique_ptr<net::CookieChangeSubscription> cookie_store_subscription_; |
| |
| // Raw pointer usage is safe because RestrictedCookieManager owns this |
| // instance and is guaranteed to outlive it. |
| const RestrictedCookieManager* const restricted_cookie_manager_; |
| |
| // The URL whose cookies this listener is interested in. |
| const GURL url_; |
| |
| // Site context in which we're used; used to determine if a cookie is accessed |
| // in a third-party context. |
| const GURL site_for_cookies_; |
| |
| // Site context in which we're used; used to check content settings. |
| const url::Origin top_frame_origin_; |
| |
| // CanonicalCookie::IncludeForRequestURL options for this listener's interest. |
| const net::CookieOptions options_; |
| |
| mojo::Remote<mojom::CookieChangeListener> mojo_listener_; |
| |
| SEQUENCE_CHECKER(sequence_checker_); |
| |
| DISALLOW_COPY_AND_ASSIGN(Listener); |
| }; |
| |
| RestrictedCookieManager::RestrictedCookieManager( |
| const mojom::RestrictedCookieManagerRole role, |
| net::CookieStore* cookie_store, |
| const CookieSettings* cookie_settings, |
| const url::Origin& origin, |
| const GURL& site_for_cookies, |
| const url::Origin& top_frame_origin, |
| mojom::NetworkContextClient* network_context_client, |
| bool is_service_worker, |
| int32_t process_id, |
| int32_t frame_id) |
| : role_(role), |
| cookie_store_(cookie_store), |
| cookie_settings_(cookie_settings), |
| origin_(origin), |
| site_for_cookies_(site_for_cookies), |
| top_frame_origin_(top_frame_origin), |
| network_context_client_(network_context_client), |
| is_service_worker_(is_service_worker), |
| process_id_(process_id), |
| frame_id_(frame_id) { |
| DCHECK(cookie_store); |
| } |
| |
| RestrictedCookieManager::~RestrictedCookieManager() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| base::LinkNode<Listener>* node = listeners_.head(); |
| while (node != listeners_.end()) { |
| Listener* listener_reference = node->value(); |
| node = node->next(); |
| // The entire list is going away, no need to remove nodes from it. |
| delete listener_reference; |
| } |
| } |
| |
| void RestrictedCookieManager::GetAllForUrl( |
| const GURL& url, |
| const GURL& site_for_cookies, |
| const url::Origin& top_frame_origin, |
| mojom::CookieManagerGetOptionsPtr options, |
| GetAllForUrlCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!ValidateAccessToCookiesAt(url, site_for_cookies, top_frame_origin)) { |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| // TODO(morlovich): Try to validate site_for_cookies as well. |
| |
| net::CookieOptions net_options = |
| MakeOptionsForGet(role_, url, site_for_cookies, cookie_settings()); |
| // TODO(https://crbug.com/977040): remove set_return_excluded_cookies() once |
| // removing deprecation warnings. |
| net_options.set_return_excluded_cookies(); |
| |
| cookie_store_->GetCookieListWithOptionsAsync( |
| url, net_options, |
| base::BindOnce(&RestrictedCookieManager::CookieListToGetAllForUrlCallback, |
| weak_ptr_factory_.GetWeakPtr(), url, site_for_cookies, |
| top_frame_origin, net_options, std::move(options), |
| std::move(callback))); |
| } |
| |
| void RestrictedCookieManager::CookieListToGetAllForUrlCallback( |
| const GURL& url, |
| const GURL& site_for_cookies, |
| const url::Origin& top_frame_origin, |
| const net::CookieOptions& net_options, |
| mojom::CookieManagerGetOptionsPtr options, |
| GetAllForUrlCallback callback, |
| const net::CookieStatusList& cookie_list, |
| const net::CookieStatusList& excluded_cookies) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| bool blocked = !cookie_settings_->IsCookieAccessAllowed(url, site_for_cookies, |
| top_frame_origin); |
| |
| std::vector<net::CanonicalCookie> result; |
| std::vector<net::CookieWithStatus> result_with_status; |
| |
| // TODO(https://crbug.com/977040): Remove once samesite tightening up is |
| // rolled out. |
| for (const auto& cookie_and_status : excluded_cookies) { |
| if (cookie_and_status.status.ShouldWarn()) { |
| result_with_status.push_back( |
| {cookie_and_status.cookie, cookie_and_status.status}); |
| } |
| } |
| |
| if (!blocked) |
| result.reserve(cookie_list.size()); |
| mojom::CookieMatchType match_type = options->match_type; |
| const std::string& match_name = options->name; |
| // TODO(https://crbug.com/993843): Use the statuses passed in |cookie_list|. |
| for (size_t i = 0; i < cookie_list.size(); ++i) { |
| const net::CanonicalCookie& cookie = cookie_list[i].cookie; |
| CookieInclusionStatus status = cookie_list[i].status; |
| const std::string& cookie_name = cookie.Name(); |
| |
| if (match_type == mojom::CookieMatchType::EQUALS) { |
| if (cookie_name != match_name) |
| continue; |
| } else if (match_type == mojom::CookieMatchType::STARTS_WITH) { |
| if (!base::StartsWith(cookie_name, match_name, |
| base::CompareCase::SENSITIVE)) { |
| continue; |
| } |
| } else { |
| NOTREACHED(); |
| } |
| |
| if (blocked) { |
| status.AddExclusionReason( |
| CookieInclusionStatus::EXCLUDE_USER_PREFERENCES); |
| } else { |
| result.push_back(cookie); |
| } |
| result_with_status.push_back({cookie, status}); |
| } |
| |
| if (network_context_client_) { |
| network_context_client_->OnCookiesRead(is_service_worker_, process_id_, |
| frame_id_, url, site_for_cookies, |
| result_with_status); |
| } |
| |
| if (blocked) { |
| DCHECK(result.empty()); |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| std::move(callback).Run(std::move(result)); |
| } |
| |
| void RestrictedCookieManager::SetCanonicalCookie( |
| const net::CanonicalCookie& cookie, |
| const GURL& url, |
| const GURL& site_for_cookies, |
| const url::Origin& top_frame_origin, |
| SetCanonicalCookieCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!ValidateAccessToCookiesAt(url, site_for_cookies, top_frame_origin)) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| // TODO(morlovich): Try to validate site_for_cookies as well. |
| bool blocked = !cookie_settings_->IsCookieAccessAllowed(url, site_for_cookies, |
| top_frame_origin); |
| |
| CookieInclusionStatus status; |
| if (blocked) |
| status.AddExclusionReason(CookieInclusionStatus::EXCLUDE_USER_PREFERENCES); |
| |
| // Don't allow URLs with leading dots like https://.some-weird-domain.com |
| // This probably never happens. |
| if (!net::cookie_util::DomainIsHostOnly(url.host())) |
| status.AddExclusionReason(CookieInclusionStatus::EXCLUDE_INVALID_DOMAIN); |
| |
| // Don't allow setting cookies on other domains. |
| // TODO(crbug.com/996786): This should never happen. This should eventually |
| // result in a renderer kill, but for now just log metrics. |
| bool domain_match = cookie.IsDomainMatch(url.host()); |
| if (!domain_match) |
| status.AddExclusionReason(CookieInclusionStatus::EXCLUDE_DOMAIN_MISMATCH); |
| UMA_HISTOGRAM_BOOLEAN( |
| "Net.RestrictedCookieManager.SetCanonicalCookieDomainMatch", |
| domain_match); |
| |
| if (!status.IsInclude()) { |
| if (network_context_client_) { |
| std::vector<net::CookieWithStatus> result_with_status = { |
| {cookie, status}}; |
| network_context_client_->OnCookiesChanged( |
| is_service_worker_, process_id_, frame_id_, url, site_for_cookies, |
| result_with_status); |
| } |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| // TODO(pwnall): Validate the CanonicalCookie fields. |
| |
| // Update the creation and last access times. |
| base::Time now = base::Time::NowFromSystemTime(); |
| // TODO(http://crbug.com/1024053): Log metrics |
| net::CookieSourceScheme source_scheme = |
| GURL::SchemeIsCryptographic(origin_.scheme()) |
| ? net::CookieSourceScheme::kSecure |
| : net::CookieSourceScheme::kNonSecure; |
| auto sanitized_cookie = std::make_unique<net::CanonicalCookie>( |
| cookie.Name(), cookie.Value(), cookie.Domain(), cookie.Path(), now, |
| cookie.ExpiryDate(), now, cookie.IsSecure(), cookie.IsHttpOnly(), |
| cookie.SameSite(), cookie.Priority(), source_scheme); |
| net::CanonicalCookie cookie_copy = *sanitized_cookie; |
| |
| net::CookieOptions options = |
| MakeOptionsForSet(role_, url, site_for_cookies, cookie_settings()); |
| cookie_store_->SetCanonicalCookieAsync( |
| std::move(sanitized_cookie), origin_.scheme(), options, |
| base::BindOnce(&RestrictedCookieManager::SetCanonicalCookieResult, |
| weak_ptr_factory_.GetWeakPtr(), url, site_for_cookies, |
| cookie_copy, options, std::move(callback))); |
| } |
| |
| void RestrictedCookieManager::SetCanonicalCookieResult( |
| const GURL& url, |
| const GURL& site_for_cookies, |
| const net::CanonicalCookie& cookie, |
| const net::CookieOptions& net_options, |
| SetCanonicalCookieCallback user_callback, |
| net::CanonicalCookie::CookieInclusionStatus status) { |
| std::vector<net::CookieWithStatus> notify; |
| // TODO(https://crbug.com/977040): Only report pure INCLUDE once samesite |
| // tightening up is rolled out. |
| DCHECK(!status.HasExclusionReason( |
| CookieInclusionStatus::EXCLUDE_USER_PREFERENCES)); |
| |
| if (network_context_client_) { |
| if (status.IsInclude() || status.ShouldWarn()) { |
| notify.push_back({cookie, status}); |
| network_context_client_->OnCookiesChanged( |
| is_service_worker_, process_id_, frame_id_, url, site_for_cookies, |
| std::move(notify)); |
| } |
| } |
| std::move(user_callback).Run(status.IsInclude()); |
| } |
| |
| void RestrictedCookieManager::AddChangeListener( |
| const GURL& url, |
| const GURL& site_for_cookies, |
| const url::Origin& top_frame_origin, |
| mojo::PendingRemote<mojom::CookieChangeListener> mojo_listener, |
| AddChangeListenerCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!ValidateAccessToCookiesAt(url, site_for_cookies, top_frame_origin)) { |
| std::move(callback).Run(); |
| return; |
| } |
| |
| net::CookieOptions net_options = |
| MakeOptionsForGet(role_, url, site_for_cookies, cookie_settings()); |
| auto listener = std::make_unique<Listener>( |
| cookie_store_, this, url, site_for_cookies, top_frame_origin, net_options, |
| std::move(mojo_listener)); |
| |
| listener->mojo_listener().set_disconnect_handler( |
| base::BindOnce(&RestrictedCookieManager::RemoveChangeListener, |
| weak_ptr_factory_.GetWeakPtr(), |
| // Safe because this owns the listener, so the listener is |
| // guaranteed to be alive for as long as the weak pointer |
| // above resolves. |
| base::Unretained(listener.get()))); |
| |
| // The linked list takes over the Listener ownership. |
| listeners_.Append(listener.release()); |
| std::move(callback).Run(); |
| } |
| |
| void RestrictedCookieManager::SetCookieFromString( |
| const GURL& url, |
| const GURL& site_for_cookies, |
| const url::Origin& top_frame_origin, |
| const std::string& cookie, |
| SetCookieFromStringCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| std::unique_ptr<net::CanonicalCookie> parsed_cookie = |
| net::CanonicalCookie::Create(url, cookie, base::Time::Now(), |
| base::nullopt /* server_time */); |
| if (!parsed_cookie) { |
| std::move(callback).Run(); |
| return; |
| } |
| |
| // Further checks (origin_, settings), as well as logging done by |
| // SetCanonicalCookie() |
| SetCanonicalCookie( |
| *parsed_cookie, url, site_for_cookies, top_frame_origin, |
| base::BindOnce([](SetCookieFromStringCallback user_callback, |
| bool success) { std::move(user_callback).Run(); }, |
| std::move(callback))); |
| } |
| |
| void RestrictedCookieManager::GetCookiesString( |
| const GURL& url, |
| const GURL& site_for_cookies, |
| const url::Origin& top_frame_origin, |
| GetCookiesStringCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Checks done by GetAllForUrl. |
| |
| // Match everything. |
| auto match_options = mojom::CookieManagerGetOptions::New(); |
| match_options->name = ""; |
| match_options->match_type = mojom::CookieMatchType::STARTS_WITH; |
| GetAllForUrl(url, site_for_cookies, top_frame_origin, |
| std::move(match_options), |
| base::BindOnce( |
| [](GetCookiesStringCallback user_callback, |
| const std::vector<net::CanonicalCookie>& cookies) { |
| std::move(user_callback) |
| .Run(net::CanonicalCookie::BuildCookieLine(cookies)); |
| }, |
| std::move(callback))); |
| } |
| |
| void RestrictedCookieManager::CookiesEnabledFor( |
| const GURL& url, |
| const GURL& site_for_cookies, |
| const url::Origin& top_frame_origin, |
| CookiesEnabledForCallback callback) { |
| if (!ValidateAccessToCookiesAt(url, site_for_cookies, top_frame_origin)) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| std::move(callback).Run(cookie_settings_->IsCookieAccessAllowed( |
| url, site_for_cookies, top_frame_origin)); |
| } |
| |
| void RestrictedCookieManager::RemoveChangeListener(Listener* listener) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| listener->RemoveFromList(); |
| delete listener; |
| } |
| |
| bool RestrictedCookieManager::ValidateAccessToCookiesAt( |
| const GURL& url, |
| const GURL& site_for_cookies, |
| const url::Origin& top_frame_origin) { |
| bool site_for_cookies_ok = |
| (site_for_cookies.is_empty() == site_for_cookies_.is_empty()); |
| if (!site_for_cookies.is_empty() && !site_for_cookies_.is_empty()) { |
| // For schemes that are not registered as "standard", the URL parser doesn't |
| // recognize the label after the scheme as the "host" but rather parses it |
| // as the "path". For such URLs, the "host" piece is parsed as empty. So we |
| // need to separately check if the two URLs are identical because we cannot |
| // rely on SameDomainOrHost because that function checks for the hosts being |
| // non-empty and equal. This may also be different for tests because some |
| // scheme-registering functions like RegisterContentSchemes are not called. |
| // |
| // This also shows up for regular file:/// URLs, when those have cookie |
| // support turned on (as they normally don't have a host name, either). |
| site_for_cookies_ok = |
| (site_for_cookies == site_for_cookies_) || |
| net::registry_controlled_domains::SameDomainOrHost( |
| site_for_cookies, site_for_cookies_, |
| net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); |
| } |
| |
| DCHECK(site_for_cookies_ok) |
| << "site_for_cookies from renderer='" << site_for_cookies |
| << "' from browser='" << site_for_cookies_ << "';"; |
| |
| bool top_frame_origin_ok = (top_frame_origin == top_frame_origin_); |
| DCHECK(top_frame_origin_ok) |
| << "top_frame_origin from renderer='" << top_frame_origin |
| << "' from browser='" << top_frame_origin_ << "';"; |
| |
| UMA_HISTOGRAM_BOOLEAN("Net.RestrictedCookieManager.SiteForCookiesOK", |
| site_for_cookies_ok); |
| UMA_HISTOGRAM_BOOLEAN("Net.RestrictedCookieManager.TopFrameOriginOK", |
| top_frame_origin_ok); |
| |
| if (origin_.IsSameOriginWith(url::Origin::Create(url))) |
| return true; |
| |
| if (url.IsAboutBlank() || url.IsAboutSrcdoc()) { |
| // Temporary mitigation for 983090, classification improvement for parts of |
| // 992587. |
| static base::debug::CrashKeyString* bound_origin = |
| base::debug::AllocateCrashKeyString( |
| "restricted_cookie_manager_bound_origin", |
| base::debug::CrashKeySize::Size256); |
| base::debug::ScopedCrashKeyString(bound_origin, origin_.GetDebugString()); |
| |
| static base::debug::CrashKeyString* url_origin = |
| base::debug::AllocateCrashKeyString( |
| "restricted_cookie_manager_url_origin", |
| base::debug::CrashKeySize::Size256); |
| base::debug::ScopedCrashKeyString( |
| url_origin, url::Origin::Create(url).GetDebugString()); |
| |
| base::debug::DumpWithoutCrashing(); |
| return false; |
| } |
| |
| mojo::ReportBadMessage("Incorrect url origin"); |
| return false; |
| } |
| |
| } // namespace network |