// Copyright 2014 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 "chrome/browser/ssl/ssl_error_handler.h" #include #include #include "base/callback_helpers.h" #include "base/macros.h" #include "base/metrics/field_trial.h" #include "base/metrics/histogram.h" #include "base/strings/stringprintf.h" #include "base/time/clock.h" #include "base/time/time.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ssl/bad_clock_blocking_page.h" #include "chrome/browser/ssl/ssl_blocking_page.h" #include "chrome/browser/ssl/ssl_cert_reporter.h" #include "components/ssl_errors/error_classification.h" #include "components/ssl_errors/error_info.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/web_contents.h" #include "net/base/net_errors.h" #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) #include "chrome/browser/captive_portal/captive_portal_service.h" #include "chrome/browser/captive_portal/captive_portal_service_factory.h" #include "chrome/browser/captive_portal/captive_portal_tab_helper.h" #include "chrome/browser/ssl/captive_portal_blocking_page.h" #endif namespace { // The delay in milliseconds before displaying the SSL interstitial. // This can be changed in tests. // - If there is a name mismatch and a suggested URL available result arrives // during this time, the user is redirected to the suggester URL. // - If a "captive portal detected" result arrives during this time, // a captive portal interstitial is displayed. // - Otherwise, an SSL interstitial is displayed. int64_t g_interstitial_delay_in_milliseconds = 2000; // Callback to call when the interstitial timer is started. Used for testing. SSLErrorHandler::TimerStartedCallback* g_timer_started_callback = nullptr; // The clock to use when deciding which error type to display. Used for testing. base::Clock* g_testing_clock = nullptr; // Events for UMA. enum SSLErrorHandlerEvent { HANDLE_ALL, SHOW_CAPTIVE_PORTAL_INTERSTITIAL_NONOVERRIDABLE, SHOW_CAPTIVE_PORTAL_INTERSTITIAL_OVERRIDABLE, SHOW_SSL_INTERSTITIAL_NONOVERRIDABLE, SHOW_SSL_INTERSTITIAL_OVERRIDABLE, WWW_MISMATCH_FOUND, WWW_MISMATCH_URL_AVAILABLE, WWW_MISMATCH_URL_NOT_AVAILABLE, SHOW_BAD_CLOCK, SSL_ERROR_HANDLER_EVENT_COUNT }; // Adds a message to console after navigation commits and then, deletes itself. // Also deletes itself if the navigation is stopped. class CommonNameMismatchRedirectObserver : public content::WebContentsObserver, public content::WebContentsUserData { public: static void AddToConsoleAfterNavigation( content::WebContents* web_contents, const std::string& request_url_hostname, const std::string& suggested_url_hostname) { web_contents->SetUserData( UserDataKey(), new CommonNameMismatchRedirectObserver( web_contents, request_url_hostname, suggested_url_hostname)); } private: CommonNameMismatchRedirectObserver(content::WebContents* web_contents, const std::string& request_url_hostname, const std::string& suggested_url_hostname) : WebContentsObserver(web_contents), web_contents_(web_contents), request_url_hostname_(request_url_hostname), suggested_url_hostname_(suggested_url_hostname) {} ~CommonNameMismatchRedirectObserver() override {} // WebContentsObserver: void NavigationStopped() override { // Deletes |this|. web_contents_->RemoveUserData(UserDataKey()); } void NavigationEntryCommitted( const content::LoadCommittedDetails& /* load_details */) override { web_contents_->GetMainFrame()->AddMessageToConsole( content::CONSOLE_MESSAGE_LEVEL_LOG, base::StringPrintf( "Redirecting navigation %s -> %s because the server presented a " "certificate valid for %s but not for %s.", request_url_hostname_.c_str(), suggested_url_hostname_.c_str(), suggested_url_hostname_.c_str(), request_url_hostname_.c_str())); web_contents_->RemoveUserData(UserDataKey()); } void WebContentsDestroyed() override { web_contents_->RemoveUserData(UserDataKey()); } content::WebContents* web_contents_; const std::string request_url_hostname_; const std::string suggested_url_hostname_; DISALLOW_COPY_AND_ASSIGN(CommonNameMismatchRedirectObserver); }; void RecordUMA(SSLErrorHandlerEvent event) { UMA_HISTOGRAM_ENUMERATION("interstitial.ssl_error_handler", event, SSL_ERROR_HANDLER_EVENT_COUNT); } #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) bool IsCaptivePortalInterstitialEnabled() { return base::FieldTrialList::FindFullName("CaptivePortalInterstitial") == "Enabled"; } #endif bool IsSSLCommonNameMismatchHandlingEnabled() { return base::FieldTrialList::FindFullName("SSLCommonNameMismatchHandling") == "Enabled"; } bool IsErrorDueToBadClock(const base::Time& now, int error) { if (ssl_errors::ErrorInfo::NetErrorToErrorType(error) != ssl_errors::ErrorInfo::CERT_DATE_INVALID) { return false; } return ssl_errors::IsUserClockInThePast(now) || ssl_errors::IsUserClockInTheFuture(now); } } // namespace DEFINE_WEB_CONTENTS_USER_DATA_KEY(SSLErrorHandler); DEFINE_WEB_CONTENTS_USER_DATA_KEY(CommonNameMismatchRedirectObserver); void SSLErrorHandler::HandleSSLError( content::WebContents* web_contents, int cert_error, const net::SSLInfo& ssl_info, const GURL& request_url, int options_mask, scoped_ptr ssl_cert_reporter, const base::Callback& callback) { DCHECK(!FromWebContents(web_contents)); SSLErrorHandler* error_handler = new SSLErrorHandler(web_contents, cert_error, ssl_info, request_url, options_mask, std::move(ssl_cert_reporter), callback); web_contents->SetUserData(UserDataKey(), error_handler); error_handler->StartHandlingError(); } // static void SSLErrorHandler::SetInterstitialDelayForTest(base::TimeDelta delay) { g_interstitial_delay_in_milliseconds = delay.InMilliseconds(); } // static void SSLErrorHandler::SetInterstitialTimerStartedCallbackForTest( TimerStartedCallback* callback) { DCHECK(!callback || !callback->is_null()); g_timer_started_callback = callback; } // static void SSLErrorHandler::SetClockForTest(base::Clock* testing_clock) { g_testing_clock = testing_clock; } SSLErrorHandler::SSLErrorHandler(content::WebContents* web_contents, int cert_error, const net::SSLInfo& ssl_info, const GURL& request_url, int options_mask, scoped_ptr ssl_cert_reporter, const base::Callback& callback) : content::WebContentsObserver(web_contents), web_contents_(web_contents), cert_error_(cert_error), ssl_info_(ssl_info), request_url_(request_url), options_mask_(options_mask), callback_(callback), profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())), ssl_cert_reporter_(std::move(ssl_cert_reporter)) {} SSLErrorHandler::~SSLErrorHandler() { } void SSLErrorHandler::StartHandlingError() { RecordUMA(HANDLE_ALL); const base::Time now = g_testing_clock == nullptr ? base::Time::NowFromSystemTime() : g_testing_clock->Now(); if (IsErrorDueToBadClock(now, cert_error_)) { ShowBadClockInterstitial(now); return; // |this| is deleted after showing the interstitial. } std::vector dns_names; ssl_info_.cert->GetDNSNames(&dns_names); DCHECK(!dns_names.empty()); GURL suggested_url; if (IsSSLCommonNameMismatchHandlingEnabled() && cert_error_ == net::ERR_CERT_COMMON_NAME_INVALID && IsErrorOverridable() && GetSuggestedUrl(dns_names, &suggested_url)) { RecordUMA(WWW_MISMATCH_FOUND); net::CertStatus extra_cert_errors = ssl_info_.cert_status ^ net::CERT_STATUS_COMMON_NAME_INVALID; // Show the SSL intersitial if |CERT_STATUS_COMMON_NAME_INVALID| is not // the only error. Need not check for captive portal in this case. // (See the comment below). if (net::IsCertStatusError(extra_cert_errors) && !net::IsCertStatusMinorError(ssl_info_.cert_status)) { ShowSSLInterstitial(); return; } CheckSuggestedUrl(suggested_url); timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds( g_interstitial_delay_in_milliseconds), this, &SSLErrorHandler::ShowSSLInterstitial); if (g_timer_started_callback) g_timer_started_callback->Run(web_contents_); // Do not check for a captive portal in this case, because a captive // portal most likely cannot serve a valid certificate which passes the // similarity check. return; } #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) CaptivePortalTabHelper* captive_portal_tab_helper = CaptivePortalTabHelper::FromWebContents(web_contents_); if (captive_portal_tab_helper) { captive_portal_tab_helper->OnSSLCertError(ssl_info_); } registrar_.Add(this, chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT, content::Source(profile_)); if (IsCaptivePortalInterstitialEnabled()) { CheckForCaptivePortal(); timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds( g_interstitial_delay_in_milliseconds), this, &SSLErrorHandler::ShowSSLInterstitial); if (g_timer_started_callback) g_timer_started_callback->Run(web_contents_); return; } #endif // Display an SSL interstitial. ShowSSLInterstitial(); } void SSLErrorHandler::CheckForCaptivePortal() { #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) CaptivePortalService* captive_portal_service = CaptivePortalServiceFactory::GetForProfile(profile_); captive_portal_service->DetectCaptivePortal(); #else NOTREACHED(); #endif } bool SSLErrorHandler::GetSuggestedUrl(const std::vector& dns_names, GURL* suggested_url) const { return CommonNameMismatchHandler::GetSuggestedUrl(request_url_, dns_names, suggested_url); } void SSLErrorHandler::CheckSuggestedUrl(const GURL& suggested_url) { scoped_refptr request_context( profile_->GetRequestContext()); common_name_mismatch_handler_.reset( new CommonNameMismatchHandler(request_url_, request_context)); common_name_mismatch_handler_->CheckSuggestedUrl( suggested_url, base::Bind(&SSLErrorHandler::CommonNameMismatchHandlerCallback, base::Unretained(this))); } void SSLErrorHandler::NavigateToSuggestedURL(const GURL& suggested_url) { content::NavigationController::LoadURLParams load_params(suggested_url); load_params.transition_type = ui::PAGE_TRANSITION_TYPED; web_contents()->GetController().LoadURLWithParams(load_params); } bool SSLErrorHandler::IsErrorOverridable() const { return SSLBlockingPage::IsOverridable(options_mask_, profile_); } void SSLErrorHandler::ShowCaptivePortalInterstitial(const GURL& landing_url) { #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) // Show captive portal blocking page. The interstitial owns the blocking page. RecordUMA(IsErrorOverridable() ? SHOW_CAPTIVE_PORTAL_INTERSTITIAL_OVERRIDABLE : SHOW_CAPTIVE_PORTAL_INTERSTITIAL_NONOVERRIDABLE); (new CaptivePortalBlockingPage(web_contents_, request_url_, landing_url, std::move(ssl_cert_reporter_), ssl_info_, callback_)) ->Show(); // Once an interstitial is displayed, no need to keep the handler around. // This is the equivalent of "delete this". It also destroys the timer. web_contents_->RemoveUserData(UserDataKey()); #else NOTREACHED(); #endif } void SSLErrorHandler::ShowSSLInterstitial() { // Show SSL blocking page. The interstitial owns the blocking page. RecordUMA(IsErrorOverridable() ? SHOW_SSL_INTERSTITIAL_OVERRIDABLE : SHOW_SSL_INTERSTITIAL_NONOVERRIDABLE); (new SSLBlockingPage(web_contents_, cert_error_, ssl_info_, request_url_, options_mask_, base::Time::NowFromSystemTime(), std::move(ssl_cert_reporter_), callback_)) ->Show(); // Once an interstitial is displayed, no need to keep the handler around. // This is the equivalent of "delete this". web_contents_->RemoveUserData(UserDataKey()); } void SSLErrorHandler::ShowBadClockInterstitial(const base::Time& now) { RecordUMA(SHOW_BAD_CLOCK); (new BadClockBlockingPage(web_contents_, cert_error_, ssl_info_, request_url_, now, std::move(ssl_cert_reporter_), callback_)) ->Show(); // Once an interstitial is displayed, no need to keep the handler around. // This is the equivalent of "delete this". web_contents_->RemoveUserData(UserDataKey()); } void SSLErrorHandler::CommonNameMismatchHandlerCallback( const CommonNameMismatchHandler::SuggestedUrlCheckResult& result, const GURL& suggested_url) { timer_.Stop(); if (result == CommonNameMismatchHandler::SuggestedUrlCheckResult:: SUGGESTED_URL_AVAILABLE) { RecordUMA(WWW_MISMATCH_URL_AVAILABLE); CommonNameMismatchRedirectObserver::AddToConsoleAfterNavigation( web_contents(), request_url_.host(), suggested_url.host()); NavigateToSuggestedURL(suggested_url); } else { RecordUMA(WWW_MISMATCH_URL_NOT_AVAILABLE); ShowSSLInterstitial(); } } void SSLErrorHandler::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) if (type == chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT) { timer_.Stop(); CaptivePortalService::Results* results = content::Details(details).ptr(); if (results->result == captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL) ShowCaptivePortalInterstitial(results->landing_url); else ShowSSLInterstitial(); } #endif } void SSLErrorHandler::DidStartNavigationToPendingEntry( const GURL& /* url */, content::NavigationController::ReloadType /* reload_type */) { // Destroy the error handler on all new navigations. This ensures that the // handler is properly recreated when a hanging page is navigated to an SSL // error, even when the tab's WebContents doesn't change. DeleteSSLErrorHandler(); } void SSLErrorHandler::NavigationStopped() { // Destroy the error handler when the page load is stopped. DeleteSSLErrorHandler(); } void SSLErrorHandler::DeleteSSLErrorHandler() { // Need to explicity deny the certificate via the callback, otherwise memory // is leaked. if (!callback_.is_null()) { base::ResetAndReturn(&callback_).Run(false); } if (common_name_mismatch_handler_) { common_name_mismatch_handler_->Cancel(); common_name_mismatch_handler_.reset(); } // Deletes |this| and also destroys the timer. web_contents_->RemoveUserData(UserDataKey()); }