// 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 "content/browser/ssl/ssl_manager.h" #include #include #include "base/bind.h" #include "base/command_line.h" #include "base/metrics/histogram_macros.h" #include "base/strings/utf_string_conversions.h" #include "base/supports_user_data.h" #include "base/trace_event/optional_trace_event.h" #include "content/browser/devtools/devtools_instrumentation.h" #include "content/browser/navigation_or_document_handle.h" #include "content/browser/renderer_host/navigation_entry_impl.h" #include "content/browser/renderer_host/render_frame_host_impl.h" #include "content/browser/ssl/ssl_error_handler.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/certificate_request_result_type.h" #include "content/public/browser/content_browser_client.h" #include "content/public/browser/devtools_agent_host.h" #include "content/public/browser/navigation_details.h" #include "content/public/browser/ssl_host_state_delegate.h" #include "content/public/common/content_client.h" #include "content/public/common/content_switches.h" #include "net/base/url_util.h" #include "net/cert/cert_status_flags.h" #include "services/metrics/public/cpp/ukm_builders.h" #include "services/metrics/public/cpp/ukm_recorder.h" #include "services/metrics/public/cpp/ukm_source_id.h" #include "third_party/blink/public/mojom/devtools/console_message.mojom.h" namespace content { namespace { const char kSSLManagerKeyName[] = "content_ssl_manager"; // Used to log type of mixed content displayed/ran, matches histogram enum // (MixedContentType). DO NOT REORDER. enum class MixedContentType { kOptionallyBlockableMixedContent = 0, kOptionallyBlockableWithCertErrors = 1, kMixedForm = 2, kBlockableMixedContent = 3, kBlockableWithCertErrors = 4, kMaxValue = kBlockableWithCertErrors, }; void OnAllowCertificate(SSLErrorHandler* handler, StoragePartition* storage_partition, SSLHostStateDelegate* state_delegate, bool record_decision, CertificateRequestResultType decision) { DCHECK(handler->ssl_info().is_valid()); switch (decision) { case CERTIFICATE_REQUEST_RESULT_TYPE_CONTINUE: // Note that we should not call SetMaxSecurityStyle here, because // the active NavigationEntry has just been deleted (in // HideInterstitialPage) and the new NavigationEntry will not be // set until DidNavigate. This is ok, because the new // NavigationEntry will have its max security style set within // DidNavigate. // // While AllowCert() executes synchronously on this thread, // ContinueRequest() gets posted to a different thread. Calling // AllowCert() first ensures deterministic ordering. if (record_decision && state_delegate) { state_delegate->AllowCert(handler->request_url().host(), *handler->ssl_info().cert.get(), handler->cert_error(), storage_partition); } handler->ContinueRequest(); return; case CERTIFICATE_REQUEST_RESULT_TYPE_DENY: handler->DenyRequest(); return; case CERTIFICATE_REQUEST_RESULT_TYPE_CANCEL: handler->CancelRequest(); return; } } class SSLManagerSet : public base::SupportsUserData::Data { public: SSLManagerSet() { } SSLManagerSet(const SSLManagerSet&) = delete; SSLManagerSet& operator=(const SSLManagerSet&) = delete; std::set& get() { return set_; } private: std::set set_; }; void LogMixedContentMetrics(MixedContentType type, ukm::SourceId source_id, ukm::UkmRecorder* recorder) { UMA_HISTOGRAM_ENUMERATION("SSL.MixedContentShown2", type); ukm::builders::SSL_MixedContentShown2(source_id) .SetType(static_cast(type)) .Record(recorder); } } // namespace // static void SSLManager::OnSSLCertificateError( const base::WeakPtr& delegate, bool is_primary_main_frame_request, const GURL& url, NavigationOrDocumentHandle* navigation_or_document, int net_error, const net::SSLInfo& ssl_info, bool fatal) { DCHECK(delegate.get()); DVLOG(1) << "OnSSLCertificateError() cert_error: " << net_error << " url: " << url.spec() << " cert_status: " << std::hex << ssl_info.cert_status; DCHECK_CURRENTLY_ON(content::BrowserThread::UI); WebContents* web_contents = nullptr; FrameTreeNode* frame_tree_node = nullptr; // This handle can be null if the request is from service worker. if (navigation_or_document) { web_contents = navigation_or_document->GetWebContents(); frame_tree_node = navigation_or_document->GetFrameTreeNode(); } std::unique_ptr handler( new SSLErrorHandler(web_contents, delegate, is_primary_main_frame_request, url, net_error, ssl_info, fatal)); if (!web_contents || !frame_tree_node) { // Requests can fail to dispatch because they don't have a WebContents. See // https://crbug.com/86537. In this case we have to make a decision in this // function. Also, if the navigation or document which have been responsible // for the request don't exist, there is no point in trying to process // further. handler->DenyRequest(); return; } // Check if we should deny certificate errors using the main frame's URL. if (GetContentClient()->browser()->ShouldDenyRequestOnCertificateError( web_contents->GetLastCommittedURL())) { handler->DenyRequest(); return; } NavigationControllerImpl& controller = frame_tree_node->navigator().controller(); controller.SetPendingNavigationSSLError(true); SSLManager* manager = controller.ssl_manager(); manager->OnCertError(std::move(handler)); } SSLManager::SSLManager(NavigationControllerImpl* controller) : controller_(controller), ssl_host_state_delegate_( controller->GetBrowserContext()->GetSSLHostStateDelegate()) { DCHECK(controller_); SSLManagerSet* managers = static_cast( controller_->GetBrowserContext()->GetUserData(kSSLManagerKeyName)); if (!managers) { auto managers_owned = std::make_unique(); managers = managers_owned.get(); controller_->GetBrowserContext()->SetUserData(kSSLManagerKeyName, std::move(managers_owned)); } managers->get().insert(this); } SSLManager::~SSLManager() { SSLManagerSet* managers = static_cast( controller_->GetBrowserContext()->GetUserData(kSSLManagerKeyName)); if (managers) managers->get().erase(this); } void SSLManager::DidCommitProvisionalLoad(const LoadCommittedDetails& details) { NavigationEntryImpl* entry = controller_->GetLastCommittedEntry(); int add_content_status_flags = 0; int remove_content_status_flags = 0; if (!details.is_main_frame || details.is_same_document) { // For subframe navigations, and for same-document main-frame navigations, // carry over content status flags from the previously committed entry. For // example, the mixed content flag shouldn't clear because of a subframe // navigation, or because of a back/forward navigation that doesn't leave // the current document. (See https://crbug.com/959571.) NavigationEntryImpl* previous_entry = controller_->GetEntryAtIndex(details.previous_entry_index); if (previous_entry) { add_content_status_flags = previous_entry->GetSSL().content_status; } } else if (!details.is_prerender_activation) { // For main-frame navigations that are not same-document and not prerender // activations, clear content status flags. These flags are set based on the // content on the page, and thus should reflect the current content, even if // the navigation was to an existing entry that already had content status // flags set. The status flags are kept for prerender activations because // |entry| points to the NavigationEntry that has just committed and it may // contain existing ssl flags which we do not want to reset. remove_content_status_flags = ~0; } if (!UpdateEntry(entry, add_content_status_flags, remove_content_status_flags, /*notify_changes=*/details.is_in_active_page)) { // Ensure the WebContents is notified that the SSL state changed when a // load is committed, in case the active navigation entry has changed. // Notification will only be called during activation if this commit is // triggered by prerendering. if (details.is_in_active_page) { NotifyDidChangeVisibleSSLState(); } } } void SSLManager::DidDisplayMixedContent() { OPTIONAL_TRACE_EVENT0("content", "SSLManager::DidDisplayMixedContent"); NavigationEntryImpl* entry = controller_->GetLastCommittedEntry(); if (entry && entry->GetURL().SchemeIsCryptographic() && entry->GetSSL().certificate) { RenderFrameHostImpl* main_frame = controller_->frame_tree().GetMainFrame(); ukm::SourceId source_id = main_frame->GetPageUkmSourceId(); LogMixedContentMetrics(MixedContentType::kOptionallyBlockableMixedContent, source_id, ukm::UkmRecorder::Get()); WebContents* contents = WebContents::FromRenderFrameHost(main_frame); if (contents) { GetContentClient()->browser()->OnDisplayInsecureContent(contents); } } UpdateLastCommittedEntry(SSLStatus::DISPLAYED_INSECURE_CONTENT, 0); } void SSLManager::DidContainInsecureFormAction() { OPTIONAL_TRACE_EVENT0("content", "SSLManager::DidContainInsecureFormAction"); NavigationEntryImpl* entry = controller_->GetLastCommittedEntry(); if (entry && entry->GetURL().SchemeIsCryptographic() && entry->GetSSL().certificate) { RenderFrameHostImpl* main_frame = controller_->frame_tree().GetMainFrame(); ukm::SourceId source_id = main_frame->GetPageUkmSourceId(); LogMixedContentMetrics(MixedContentType::kMixedForm, source_id, ukm::UkmRecorder::Get()); } UpdateLastCommittedEntry(SSLStatus::DISPLAYED_FORM_WITH_INSECURE_ACTION, 0); } void SSLManager::DidDisplayContentWithCertErrors() { NavigationEntryImpl* entry = controller_->GetLastCommittedEntry(); if (!entry) return; if (entry->GetURL().SchemeIsCryptographic() && entry->GetSSL().certificate) { // Only record information about subresources with cert errors if the // main page is HTTPS with a valid certificate. if (!net::IsCertStatusError(entry->GetSSL().cert_status)) { RenderFrameHostImpl* main_frame = controller_->frame_tree().GetMainFrame(); ukm::SourceId source_id = main_frame->GetPageUkmSourceId(); LogMixedContentMetrics( MixedContentType::kOptionallyBlockableWithCertErrors, source_id, ukm::UkmRecorder::Get()); } UpdateLastCommittedEntry(SSLStatus::DISPLAYED_CONTENT_WITH_CERT_ERRORS, 0); } } void SSLManager::DidRunMixedContent(const GURL& security_origin) { NavigationEntryImpl* entry = controller_->GetLastCommittedEntry(); if (!entry) return; if (entry->GetURL().SchemeIsCryptographic() && entry->GetSSL().certificate) { RenderFrameHostImpl* main_frame = controller_->frame_tree().GetMainFrame(); ukm::SourceId source_id = main_frame->GetPageUkmSourceId(); LogMixedContentMetrics(MixedContentType::kBlockableMixedContent, source_id, ukm::UkmRecorder::Get()); } SiteInstance* site_instance = entry->site_instance(); if (!site_instance) return; if (ssl_host_state_delegate_) { ssl_host_state_delegate_->HostRanInsecureContent( security_origin.host(), site_instance->GetProcess()->GetID(), SSLHostStateDelegate::MIXED_CONTENT); } // TODO(crbug.com/1320302): Ensure proper notify_changes is passed to // UpdateEntry. UpdateEntry(entry, 0, 0, /*notify_changes=*/true); NotifySSLInternalStateChanged(controller_->GetBrowserContext()); } void SSLManager::DidRunContentWithCertErrors(const GURL& security_origin) { NavigationEntryImpl* entry = controller_->GetLastCommittedEntry(); if (!entry) return; // Only record information about subresources with cert errors if the // main page is HTTPS with a valid certificate. if (entry->GetURL().SchemeIsCryptographic() && entry->GetSSL().certificate && !net::IsCertStatusError(entry->GetSSL().cert_status)) { RenderFrameHostImpl* main_frame = controller_->frame_tree().GetMainFrame(); ukm::SourceId source_id = main_frame->GetPageUkmSourceId(); LogMixedContentMetrics(MixedContentType::kBlockableWithCertErrors, source_id, ukm::UkmRecorder::Get()); } SiteInstance* site_instance = entry->site_instance(); if (!site_instance) return; if (ssl_host_state_delegate_) { ssl_host_state_delegate_->HostRanInsecureContent( security_origin.host(), site_instance->GetProcess()->GetID(), SSLHostStateDelegate::CERT_ERRORS_CONTENT); } // TODO(crbug.com/1320302): Ensure proper notify_changes is passed to // UpdateEntry. UpdateEntry(entry, 0, 0, /*notify_changes=*/true); NotifySSLInternalStateChanged(controller_->GetBrowserContext()); } void SSLManager::OnCertError(std::unique_ptr handler) { // First we check if we know the policy for this error. DCHECK(handler->ssl_info().is_valid()); SSLHostStateDelegate::CertJudgment judgment; if (net::IsLocalhost(handler->request_url()) && base::CommandLine::ForCurrentProcess()->HasSwitch( switches::kAllowInsecureLocalhost)) { // If the appropriate flag is set, let requests on localhost go // through even if there are certificate errors. Errors on localhost // are unlikely to indicate actual security problems. judgment = SSLHostStateDelegate::ALLOWED; } else if (ssl_host_state_delegate_) { judgment = ssl_host_state_delegate_->QueryPolicy( handler->request_url().host(), *handler->ssl_info().cert.get(), handler->cert_error(), controller_->frame_tree().GetMainFrame()->GetStoragePartition()); } else { judgment = SSLHostStateDelegate::DENIED; } if (judgment == SSLHostStateDelegate::ALLOWED) { handler->ContinueRequest(); return; } DCHECK(net::IsCertificateError(handler->cert_error())); OnCertErrorInternal(std::move(handler)); } void SSLManager::DidStartResourceResponse( const url::SchemeHostPort& final_response_url, bool has_certificate_errors) { const std::string& scheme = final_response_url.scheme(); const std::string& host = final_response_url.host(); if (!GURL::SchemeIsCryptographic(scheme) || has_certificate_errors) return; // If the scheme is https: or wss and the cert did not have any errors, revoke // any previous decisions that have occurred. if (!ssl_host_state_delegate_ || !ssl_host_state_delegate_->HasAllowException( host, controller_->frame_tree().GetMainFrame()->GetStoragePartition())) { return; } // If there's no certificate error, a good certificate has been seen, so // clear out any exceptions that were made by the user for bad // certificates. This intentionally does not apply to cached resources // (see https://crbug.com/634553 for an explanation). ssl_host_state_delegate_->RevokeUserAllowExceptions(host); } void SSLManager::OnCertErrorInternal(std::unique_ptr handler) { WebContents* web_contents = handler->web_contents(); int cert_error = handler->cert_error(); const net::SSLInfo& ssl_info = handler->ssl_info(); const GURL& request_url = handler->request_url(); bool is_primary_main_frame_request = handler->is_primary_main_frame_request(); bool fatal = handler->fatal(); base::RepeatingCallback callback = base::BindRepeating( &OnAllowCertificate, base::Owned(handler.release()), controller_->frame_tree().GetMainFrame()->GetStoragePartition(), ssl_host_state_delegate_); if (devtools_instrumentation::HandleCertificateError( web_contents, cert_error, request_url, base::BindRepeating(callback, false))) { return; } GetContentClient()->browser()->AllowCertificateError( web_contents, cert_error, ssl_info, request_url, is_primary_main_frame_request, fatal, base::BindOnce(std::move(callback), true)); } bool SSLManager::UpdateEntry(NavigationEntryImpl* entry, int add_content_status_flags, int remove_content_status_flags, bool notify_changes) { // We don't always have a navigation entry to update, for example in the // case of the Web Inspector. if (!entry) return false; SSLStatus original_ssl_status = entry->GetSSL(); // Copy! entry->GetSSL().initialized = true; entry->GetSSL().content_status &= ~remove_content_status_flags; entry->GetSSL().content_status |= add_content_status_flags; SiteInstance* site_instance = entry->site_instance(); // Note that |site_instance| can be NULL here because NavigationEntries don't // necessarily have site instances. Without a process, the entry can't // possibly have insecure content. See bug https://crbug.com/12423. if (site_instance && ssl_host_state_delegate_) { const absl::optional& entry_origin = entry->root_node()->frame_entry->committed_origin(); // In some cases (e.g., unreachable URLs), navigation entries might not have // origins attached to them. We don't care about tracking mixed content for // those cases. if (entry_origin.has_value()) { const std::string& host = entry_origin->host(); int process_id = site_instance->GetProcess()->GetID(); if (ssl_host_state_delegate_->DidHostRunInsecureContent( host, process_id, SSLHostStateDelegate::MIXED_CONTENT)) { entry->GetSSL().content_status |= SSLStatus::RAN_INSECURE_CONTENT; } // Only record information about subresources with cert errors if the // main page is HTTPS with a certificate. if (entry->GetURL().SchemeIsCryptographic() && entry->GetSSL().certificate && ssl_host_state_delegate_->DidHostRunInsecureContent( host, process_id, SSLHostStateDelegate::CERT_ERRORS_CONTENT)) { entry->GetSSL().content_status |= SSLStatus::RAN_CONTENT_WITH_CERT_ERRORS; } } } if (entry->GetSSL().initialized != original_ssl_status.initialized || entry->GetSSL().content_status != original_ssl_status.content_status) { if (notify_changes) { NotifyDidChangeVisibleSSLState(); } return true; } return false; } void SSLManager::UpdateLastCommittedEntry(int add_content_status_flags, int remove_content_status_flags) { NavigationEntryImpl* entry; if (controller_->frame_tree().type() == FrameTree::Type::kFencedFrame) { // Only the primary frame tree's NavigationEntries are exposed outside of // content, so the primary frame tree's NavigationController needs to // represent an aggregate view of the security state of its inner frame // trees. RenderFrameHost* rfh = controller_->frame_tree().root()->current_frame_host(); DCHECK(rfh); WebContentsImpl* contents = static_cast( WebContents::FromRenderFrameHost(rfh->GetOutermostMainFrame())); // TODO(crbug.com/1232528): Ensure only fenced frames owned by active pages // can modify this. entry = contents->GetController().GetLastCommittedEntry(); } else { entry = controller_->GetLastCommittedEntry(); } if (!entry) return; // TODO(crbug.com/1320302): Ensure proper notify_changes is passed to // UpdateEntry. UpdateEntry(entry, add_content_status_flags, remove_content_status_flags, /*notify_changes=*/true); } void SSLManager::NotifyDidChangeVisibleSSLState() { WebContentsImpl* contents = static_cast(controller_->DeprecatedGetWebContents()); contents->DidChangeVisibleSecurityState(); } // static void SSLManager::NotifySSLInternalStateChanged(BrowserContext* context) { SSLManagerSet* managers = static_cast(context->GetUserData(kSSLManagerKeyName)); for (auto* manager : managers->get()) { // TODO(crbug.com/1320302): Ensure proper notify_changes is passed to // UpdateEntry. manager->UpdateEntry(manager->controller()->GetLastCommittedEntry(), 0, 0, /*notify_changes=*/true); } } } // namespace content