| // Copyright (c) 2012 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 "content/browser/ssl/ssl_manager.h" |
| |
| #include <set> |
| |
| #include "base/bind.h" |
| #include "base/macros.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/supports_user_data.h" |
| #include "content/browser/devtools/devtools_agent_host_impl.h" |
| #include "content/browser/devtools/protocol/security_handler.h" |
| #include "content/browser/frame_host/navigation_entry_impl.h" |
| #include "content/browser/loader/resource_dispatcher_host_impl.h" |
| #include "content/browser/loader/resource_request_info_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_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 "net/url_request/url_request.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| const char kSSLManagerKeyName[] = "content_ssl_manager"; |
| |
| // Events for UMA. Do not reorder or change! |
| enum SSLGoodCertSeenEvent { |
| NO_PREVIOUS_EXCEPTION = 0, |
| HAD_PREVIOUS_EXCEPTION = 1, |
| SSL_GOOD_CERT_SEEN_EVENT_MAX = 2 |
| }; |
| |
| void OnAllowCertificateWithRecordDecision( |
| bool record_decision, |
| const base::Callback<void(bool, content::CertificateRequestResultType)>& |
| callback, |
| CertificateRequestResultType decision) { |
| callback.Run(record_decision, decision); |
| } |
| |
| void OnAllowCertificate(SSLErrorHandler* handler, |
| 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()); |
| } |
| 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() { |
| } |
| |
| std::set<SSLManager*>& get() { return set_; } |
| |
| private: |
| std::set<SSLManager*> set_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SSLManagerSet); |
| }; |
| |
| void HandleSSLErrorOnUI( |
| const base::Callback<WebContents*(void)>& web_contents_getter, |
| const base::WeakPtr<SSLErrorHandler::Delegate>& delegate, |
| const ResourceType resource_type, |
| const GURL& url, |
| const net::SSLInfo& ssl_info, |
| bool fatal) { |
| content::WebContents* web_contents = web_contents_getter.Run(); |
| std::unique_ptr<SSLErrorHandler> handler(new SSLErrorHandler( |
| web_contents, delegate, resource_type, url, ssl_info, fatal)); |
| |
| if (!web_contents) { |
| // 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, so we ignore revocation check failures. |
| if (net::IsCertStatusMinorError(ssl_info.cert_status)) { |
| handler->ContinueRequest(); |
| } else { |
| handler->CancelRequest(); |
| } |
| return; |
| } |
| |
| NavigationControllerImpl* controller = |
| static_cast<NavigationControllerImpl*>(&web_contents->GetController()); |
| controller->SetPendingNavigationSSLError(true); |
| |
| SSLManager* manager = controller->ssl_manager(); |
| manager->OnCertError(std::move(handler)); |
| } |
| |
| } // namespace |
| |
| // static |
| void SSLManager::OnSSLCertificateError( |
| const base::WeakPtr<SSLErrorHandler::Delegate>& delegate, |
| const ResourceType resource_type, |
| const GURL& url, |
| const base::Callback<WebContents*(void)>& web_contents_getter, |
| const net::SSLInfo& ssl_info, |
| bool fatal) { |
| DCHECK(delegate.get()); |
| DVLOG(1) << "OnSSLCertificateError() cert_error: " |
| << net::MapCertStatusToNetError(ssl_info.cert_status) |
| << " resource_type: " << resource_type |
| << " url: " << url.spec() |
| << " cert_status: " << std::hex << ssl_info.cert_status; |
| |
| // A certificate error occurred. Construct a SSLErrorHandler object |
| // on the UI thread for processing. |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::Bind(&HandleSSLErrorOnUI, web_contents_getter, delegate, |
| resource_type, url, ssl_info, fatal)); |
| } |
| |
| // static |
| void SSLManager::OnSSLCertificateSubresourceError( |
| const base::WeakPtr<SSLErrorHandler::Delegate>& delegate, |
| const GURL& url, |
| int render_process_id, |
| int render_frame_id, |
| const net::SSLInfo& ssl_info, |
| bool fatal) { |
| OnSSLCertificateError(delegate, RESOURCE_TYPE_SUB_RESOURCE, url, |
| base::Bind(&WebContentsImpl::FromRenderFrameHostID, |
| render_process_id, render_frame_id), |
| ssl_info, fatal); |
| } |
| |
| SSLManager::SSLManager(NavigationControllerImpl* controller) |
| : controller_(controller), |
| ssl_host_state_delegate_( |
| controller->GetBrowserContext()->GetSSLHostStateDelegate()) { |
| DCHECK(controller_); |
| |
| SSLManagerSet* managers = static_cast<SSLManagerSet*>( |
| controller_->GetBrowserContext()->GetUserData(kSSLManagerKeyName)); |
| if (!managers) { |
| managers = new SSLManagerSet; |
| controller_->GetBrowserContext()->SetUserData(kSSLManagerKeyName, managers); |
| } |
| managers->get().insert(this); |
| } |
| |
| SSLManager::~SSLManager() { |
| SSLManagerSet* managers = static_cast<SSLManagerSet*>( |
| controller_->GetBrowserContext()->GetUserData(kSSLManagerKeyName)); |
| managers->get().erase(this); |
| } |
| |
| void SSLManager::DidCommitProvisionalLoad(const LoadCommittedDetails& details) { |
| NavigationEntryImpl* entry = controller_->GetLastCommittedEntry(); |
| int content_status_flags = 0; |
| if (!details.is_main_frame) { |
| // If it wasn't a main-frame navigation, then carry over content |
| // status flags. (For example, the mixed content flag shouldn't |
| // clear because of a frame navigation.) |
| NavigationEntryImpl* previous_entry = |
| controller_->GetEntryAtIndex(details.previous_entry_index); |
| if (previous_entry) { |
| content_status_flags = previous_entry->GetSSL().content_status; |
| } |
| } |
| UpdateEntry(entry, content_status_flags, 0); |
| // Always notify the WebContents that the SSL state changed when a |
| // load is committed, in case the active navigation entry has changed. |
| NotifyDidChangeVisibleSSLState(); |
| } |
| |
| void SSLManager::DidDisplayMixedContent() { |
| UpdateLastCommittedEntry(SSLStatus::DISPLAYED_INSECURE_CONTENT, 0); |
| } |
| |
| void SSLManager::DidDisplayContentWithCertErrors() { |
| NavigationEntryImpl* entry = controller_->GetLastCommittedEntry(); |
| if (!entry) |
| return; |
| // Only record information about subresources with cert errors if the |
| // main page is HTTPS with a certificate. |
| if (entry->GetURL().SchemeIsCryptographic() && entry->GetSSL().certificate) { |
| UpdateLastCommittedEntry(SSLStatus::DISPLAYED_CONTENT_WITH_CERT_ERRORS, 0); |
| } |
| } |
| |
| void SSLManager::DidShowPasswordInputOnHttp() { |
| UpdateLastCommittedEntry(SSLStatus::DISPLAYED_PASSWORD_FIELD_ON_HTTP, 0); |
| } |
| |
| void SSLManager::DidHideAllPasswordInputsOnHttp() { |
| UpdateLastCommittedEntry(0, SSLStatus::DISPLAYED_PASSWORD_FIELD_ON_HTTP); |
| } |
| |
| void SSLManager::DidShowCreditCardInputOnHttp() { |
| UpdateLastCommittedEntry(SSLStatus::DISPLAYED_CREDIT_CARD_FIELD_ON_HTTP, 0); |
| } |
| |
| void SSLManager::DidRunMixedContent(const GURL& security_origin) { |
| NavigationEntryImpl* entry = controller_->GetLastCommittedEntry(); |
| if (!entry) |
| return; |
| |
| 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); |
| } |
| UpdateEntry(entry, 0, 0); |
| NotifySSLInternalStateChanged(controller_->GetBrowserContext()); |
| } |
| |
| void SSLManager::DidRunContentWithCertErrors(const GURL& security_origin) { |
| NavigationEntryImpl* entry = controller_->GetLastCommittedEntry(); |
| if (!entry) |
| return; |
| |
| 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); |
| } |
| UpdateEntry(entry, 0, 0); |
| NotifySSLInternalStateChanged(controller_->GetBrowserContext()); |
| } |
| |
| void SSLManager::OnCertError(std::unique_ptr<SSLErrorHandler> handler) { |
| bool expired_previous_decision = false; |
| // First we check if we know the policy for this error. |
| DCHECK(handler->ssl_info().is_valid()); |
| SSLHostStateDelegate::CertJudgment judgment = |
| ssl_host_state_delegate_ |
| ? ssl_host_state_delegate_->QueryPolicy( |
| handler->request_url().host(), *handler->ssl_info().cert.get(), |
| handler->cert_error(), &expired_previous_decision) |
| : SSLHostStateDelegate::DENIED; |
| |
| if (judgment == SSLHostStateDelegate::ALLOWED) { |
| handler->ContinueRequest(); |
| return; |
| } |
| |
| // For all other hosts, which must be DENIED, a blocking page is shown to the |
| // user every time they come back to the page. |
| int options_mask = 0; |
| switch (handler->cert_error()) { |
| case net::ERR_CERT_COMMON_NAME_INVALID: |
| case net::ERR_CERT_DATE_INVALID: |
| case net::ERR_CERT_AUTHORITY_INVALID: |
| case net::ERR_CERT_WEAK_SIGNATURE_ALGORITHM: |
| case net::ERR_CERT_WEAK_KEY: |
| case net::ERR_CERT_NAME_CONSTRAINT_VIOLATION: |
| case net::ERR_CERT_VALIDITY_TOO_LONG: |
| case net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED: |
| if (!handler->fatal()) |
| options_mask |= OVERRIDABLE; |
| else |
| options_mask |= STRICT_ENFORCEMENT; |
| if (expired_previous_decision) |
| options_mask |= EXPIRED_PREVIOUS_DECISION; |
| OnCertErrorInternal(std::move(handler), options_mask); |
| break; |
| case net::ERR_CERT_NO_REVOCATION_MECHANISM: |
| // Ignore this error. |
| handler->ContinueRequest(); |
| break; |
| case net::ERR_CERT_UNABLE_TO_CHECK_REVOCATION: |
| // We ignore this error but will show a warning status in the location |
| // bar. |
| handler->ContinueRequest(); |
| break; |
| case net::ERR_CERT_CONTAINS_ERRORS: |
| case net::ERR_CERT_REVOKED: |
| case net::ERR_CERT_INVALID: |
| case net::ERR_SSL_WEAK_SERVER_EPHEMERAL_DH_KEY: |
| case net::ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN: |
| if (handler->fatal()) |
| options_mask |= STRICT_ENFORCEMENT; |
| if (expired_previous_decision) |
| options_mask |= EXPIRED_PREVIOUS_DECISION; |
| OnCertErrorInternal(std::move(handler), options_mask); |
| break; |
| default: |
| NOTREACHED(); |
| handler->CancelRequest(); |
| break; |
| } |
| } |
| |
| void SSLManager::DidStartResourceResponse(const GURL& url, |
| bool has_certificate, |
| net::CertStatus ssl_cert_status) { |
| if (has_certificate && url.SchemeIsCryptographic() && |
| !net::IsCertStatusError(ssl_cert_status)) { |
| // If the scheme is https: or wss: *and* the security info for the |
| // cert has been set (i.e. the cert id is not 0) and the cert did |
| // not have any errors, revoke any previous decisions that |
| // have occurred. If the cert info has not been set, do nothing since it |
| // isn't known if the connection was actually a valid connection or if it |
| // had a cert error. |
| SSLGoodCertSeenEvent event = NO_PREVIOUS_EXCEPTION; |
| if (ssl_host_state_delegate_ && |
| ssl_host_state_delegate_->HasAllowException(url.host())) { |
| // 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(url.host()); |
| event = HAD_PREVIOUS_EXCEPTION; |
| } |
| UMA_HISTOGRAM_ENUMERATION("interstitial.ssl.good_cert_seen", event, |
| SSL_GOOD_CERT_SEEN_EVENT_MAX); |
| } |
| } |
| |
| void SSLManager::OnCertErrorInternal(std::unique_ptr<SSLErrorHandler> handler, |
| int options_mask) { |
| bool overridable = (options_mask & OVERRIDABLE) != 0; |
| bool strict_enforcement = (options_mask & STRICT_ENFORCEMENT) != 0; |
| bool expired_previous_decision = |
| (options_mask & EXPIRED_PREVIOUS_DECISION) != 0; |
| |
| 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(); |
| ResourceType resource_type = handler->resource_type(); |
| |
| base::Callback<void(bool, content::CertificateRequestResultType)> callback = |
| base::Bind(&OnAllowCertificate, base::Owned(handler.release()), |
| ssl_host_state_delegate_); |
| |
| DevToolsAgentHostImpl* agent_host = static_cast<DevToolsAgentHostImpl*>( |
| DevToolsAgentHost::GetOrCreateFor(web_contents).get()); |
| protocol::SecurityHandler* security_handler = |
| protocol::SecurityHandler::FromAgentHost(agent_host); |
| if (!security_handler || |
| !security_handler->NotifyCertificateError( |
| cert_error, request_url, |
| base::Bind(&OnAllowCertificateWithRecordDecision, false, callback))) { |
| GetContentClient()->browser()->AllowCertificateError( |
| web_contents, cert_error, ssl_info, request_url, resource_type, |
| overridable, strict_enforcement, expired_previous_decision, |
| base::Bind(&OnAllowCertificateWithRecordDecision, true, callback)); |
| } |
| } |
| |
| void SSLManager::UpdateEntry(NavigationEntryImpl* entry, |
| int add_content_status_flags, |
| int remove_content_status_flags) { |
| // We don't always have a navigation entry to update, for example in the |
| // case of the Web Inspector. |
| if (!entry) |
| return; |
| |
| SSLStatus original_ssl_status = entry->GetSSL(); // Copy! |
| entry->GetSSL().initialized = true; |
| entry->GetSSL().content_status |= add_content_status_flags; |
| entry->GetSSL().content_status &= ~remove_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_) { |
| std::string host = entry->GetURL().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().Equals(original_ssl_status)) |
| NotifyDidChangeVisibleSSLState(); |
| } |
| |
| void SSLManager::UpdateLastCommittedEntry(int add_content_status_flags, |
| int remove_content_status_flags) { |
| NavigationEntryImpl* entry = controller_->GetLastCommittedEntry(); |
| if (!entry) |
| return; |
| UpdateEntry(entry, add_content_status_flags, remove_content_status_flags); |
| } |
| |
| void SSLManager::NotifyDidChangeVisibleSSLState() { |
| WebContentsImpl* contents = |
| static_cast<WebContentsImpl*>(controller_->delegate()->GetWebContents()); |
| contents->DidChangeVisibleSecurityState(); |
| } |
| |
| // static |
| void SSLManager::NotifySSLInternalStateChanged(BrowserContext* context) { |
| SSLManagerSet* managers = |
| static_cast<SSLManagerSet*>(context->GetUserData(kSSLManagerKeyName)); |
| |
| for (std::set<SSLManager*>::iterator i = managers->get().begin(); |
| i != managers->get().end(); ++i) { |
| (*i)->UpdateEntry((*i)->controller()->GetLastCommittedEntry(), 0, 0); |
| } |
| } |
| |
| } // namespace content |