| // Copyright 2020 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/safe_browsing/user_interaction_observer.h" |
| |
| #include <string> |
| |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "components/omnibox/browser/omnibox_prefs.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/safe_browsing/buildflags.h" |
| #include "components/safe_browsing/core/common/features.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/buildflags/buildflags.h" |
| #include "third_party/blink/public/common/input/web_mouse_event.h" |
| #include "ui/events/keycodes/keyboard_codes.h" |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| #include "extensions/browser/extension_registry.h" |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| using blink::WebInputEvent; |
| |
| namespace { |
| // Id for extension that enables users to report sites to Safe Browsing. |
| const char kPreventElisionExtensionId[] = "jknemblkbdhdcpllfgbfekkdciegfboi"; |
| } // namespace |
| |
| namespace safe_browsing { |
| |
| const char kDelayedWarningsHistogram[] = "SafeBrowsing.DelayedWarnings.Event"; |
| const char kDelayedWarningsTimeOnPageHistogram[] = |
| "SafeBrowsing.DelayedWarnings.TimeOnPage"; |
| |
| const char kDelayedWarningsWithElisionDisabledHistogram[] = |
| "SafeBrowsing.DelayedWarnings.Event_UrlElisionDisabled"; |
| const char kDelayedWarningsTimeOnPageWithElisionDisabledHistogram[] = |
| "SafeBrowsing.DelayedWarnings.TimeOnPage_UrlElisionDisabled"; |
| |
| namespace { |
| const char kWebContentsUserDataKey[] = |
| "web_contents_safe_browsing_user_interaction_observer"; |
| |
| bool IsUrlElisionDisabled(Profile* profile, |
| const char* suspicious_site_reporter_extension_id) { |
| if (profile && |
| profile->GetPrefs()->GetBoolean(omnibox::kPreventUrlElisionsInOmnibox)) { |
| return true; |
| } |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| DCHECK(suspicious_site_reporter_extension_id); |
| if (profile && extensions::ExtensionRegistry::Get(profile) |
| ->enabled_extensions() |
| .Contains(suspicious_site_reporter_extension_id)) { |
| return true; |
| } |
| #endif |
| return false; |
| } |
| |
| } // namespace |
| |
| // static |
| const char* SafeBrowsingUserInteractionObserver:: |
| suspicious_site_reporter_extension_id_ = kPreventElisionExtensionId; |
| |
| SafeBrowsingUserInteractionObserver::SafeBrowsingUserInteractionObserver( |
| content::WebContents* web_contents, |
| const security_interstitials::UnsafeResource& resource, |
| bool is_main_frame, |
| scoped_refptr<SafeBrowsingUIManager> ui_manager) |
| : content::WebContentsObserver(web_contents), |
| web_contents_(web_contents), |
| resource_(resource), |
| ui_manager_(ui_manager), |
| creation_time_(base::Time::Now()), |
| clock_(base::DefaultClock::GetInstance()) { |
| DCHECK(base::FeatureList::IsEnabled(kDelayedWarnings)); |
| key_press_callback_ = |
| base::BindRepeating(&SafeBrowsingUserInteractionObserver::HandleKeyPress, |
| base::Unretained(this)); |
| mouse_event_callback_ = base::BindRepeating( |
| &SafeBrowsingUserInteractionObserver::HandleMouseEvent, |
| base::Unretained(this)); |
| // Pass a callback to the RenderWidgetHost instead of implementing |
| // WebContentsObserver::DidGetUserInteraction(). The reason for this is that |
| // RenderWidgetHost handles keyboard events earlier and the callback can |
| // indicate that it wants the key press to be ignored. |
| // (DidGetUserInteraction() can only observe and not cancel the event.) |
| content::RenderWidgetHost* widget = |
| web_contents->GetMainFrame()->GetRenderWidgetHost(); |
| widget->AddKeyPressEventCallback(key_press_callback_); |
| widget->AddMouseEventCallback(mouse_event_callback_); |
| |
| // Observe permission bubble events. |
| permissions::PermissionRequestManager* permission_request_manager = |
| permissions::PermissionRequestManager::FromWebContents(web_contents); |
| if (permission_request_manager) { |
| permission_request_manager->AddObserver(this); |
| } |
| RecordUMA(DelayedWarningEvent::kPageLoaded); |
| } |
| |
| SafeBrowsingUserInteractionObserver::~SafeBrowsingUserInteractionObserver() { |
| permissions::PermissionRequestManager* permission_request_manager = |
| permissions::PermissionRequestManager::FromWebContents(web_contents()); |
| if (permission_request_manager) { |
| permission_request_manager->RemoveObserver(this); |
| } |
| web_contents_->GetMainFrame() |
| ->GetRenderWidgetHost() |
| ->RemoveKeyPressEventCallback(key_press_callback_); |
| web_contents_->GetMainFrame() |
| ->GetRenderWidgetHost() |
| ->RemoveMouseEventCallback(mouse_event_callback_); |
| } |
| |
| // static |
| void SafeBrowsingUserInteractionObserver::CreateForWebContents( |
| content::WebContents* web_contents, |
| const security_interstitials::UnsafeResource& resource, |
| bool is_main_frame, |
| scoped_refptr<SafeBrowsingUIManager> ui_manager) { |
| // This method is called for all unsafe resources on |web_contents|. Only |
| // create an observer if there isn't one. |
| // TODO(crbug.com/1057157): The observer should observe all unsafe resources |
| // instead of the first one only. |
| if (FromWebContents(web_contents)) { |
| return; |
| } |
| DCHECK(!web_contents->IsPortal()); |
| auto observer = std::make_unique<SafeBrowsingUserInteractionObserver>( |
| web_contents, resource, is_main_frame, ui_manager); |
| web_contents->SetUserData(kWebContentsUserDataKey, std::move(observer)); |
| } |
| |
| // static |
| SafeBrowsingUserInteractionObserver* |
| SafeBrowsingUserInteractionObserver::FromWebContents( |
| content::WebContents* web_contents) { |
| return static_cast<SafeBrowsingUserInteractionObserver*>( |
| web_contents->GetUserData(kWebContentsUserDataKey)); |
| } |
| |
| void SafeBrowsingUserInteractionObserver::RenderFrameHostChanged( |
| content::RenderFrameHost* old_frame, |
| content::RenderFrameHost* new_frame) { |
| // We currently only insert callbacks on the widget for the top-level main |
| // frame. |
| if (new_frame != web_contents()->GetMainFrame()) |
| return; |
| // The `old_frame` is null when the `new_frame` is the initial |
| // RenderFrameHost, which we already attached to in the constructor. |
| if (!old_frame) |
| return; |
| content::RenderWidgetHost* old_widget = old_frame->GetRenderWidgetHost(); |
| old_widget->RemoveKeyPressEventCallback(key_press_callback_); |
| old_widget->RemoveMouseEventCallback(mouse_event_callback_); |
| content::RenderWidgetHost* new_widget = new_frame->GetRenderWidgetHost(); |
| new_widget->AddKeyPressEventCallback(key_press_callback_); |
| new_widget->AddMouseEventCallback(mouse_event_callback_); |
| } |
| |
| void SafeBrowsingUserInteractionObserver::WebContentsDestroyed() { |
| CleanUp(); |
| Detach(); |
| } |
| |
| void SafeBrowsingUserInteractionObserver::DidFinishNavigation( |
| content::NavigationHandle* handle) { |
| // Remove the observer on a top frame navigation to another page. The user is |
| // now on another page so we don't need to wait for an interaction. |
| if (!handle->IsInPrimaryMainFrame() || handle->IsSameDocument()) { |
| return; |
| } |
| // If this is the first navigation we are seeing, it must be the |
| // navigation that caused this observer to be created. |
| // As an example, if the user navigates to http://test.site, the order of |
| // events are: |
| // 1. SafeBrowsingUrlCheckerImpl detects that the URL should be blocked with |
| // an interstitial. |
| // 2. It delays the interstitial and creates an instance of this class. |
| // 3. DidFinishNavigation() of this class is called. |
| // |
| // This means that the first time we are here, we should ignore this event |
| // because it's not an interesting navigation. We only want to handle the |
| // navigations that follow. |
| if (!initial_navigation_finished_) { |
| initial_navigation_finished_ = true; |
| return; |
| } |
| // If a download happens when an instance of this observer is attached to |
| // the WebContents, DelayedNavigationThrottle cancels the download. As a |
| // result, the page should remain unchanged on downloads. Record a metric and |
| // ignore this cancelled navigation. |
| if (handle->IsDownload()) { |
| RecordUMA(DelayedWarningEvent::kDownloadCancelled); |
| return; |
| } |
| Detach(); |
| // DO NOT add code past this point. |this| is destroyed. |
| } |
| |
| void SafeBrowsingUserInteractionObserver::Detach() { |
| if (!interstitial_shown_) { |
| RecordUMA(DelayedWarningEvent::kWarningNotShown); |
| } |
| base::TimeDelta time_on_page = clock_->Now() - creation_time_; |
| if (IsUrlElisionDisabled( |
| Profile::FromBrowserContext(web_contents()->GetBrowserContext()), |
| suspicious_site_reporter_extension_id_)) { |
| base::UmaHistogramLongTimes( |
| kDelayedWarningsTimeOnPageWithElisionDisabledHistogram, time_on_page); |
| } else { |
| base::UmaHistogramLongTimes(kDelayedWarningsTimeOnPageHistogram, |
| time_on_page); |
| } |
| web_contents()->RemoveUserData(kWebContentsUserDataKey); |
| // DO NOT add code past this point. |this| is destroyed. |
| } |
| |
| void SafeBrowsingUserInteractionObserver::DidToggleFullscreenModeForTab( |
| bool entered_fullscreen, |
| bool will_cause_resize) { |
| // This class is only instantiated upon a navigation. If a page is in |
| // fullscreen mode, any navigation away from it should exit fullscreen. This |
| // means that this class is never instantiated while the current web contents |
| // is in fullscreen mode, so |entered_fullscreen| should never be false when |
| // this method is called for the first time. However, we don't know if it's |
| // guaranteed for a page to not be in fullscreen upon navigation, so we just |
| // ignore this event if the page exited fullscreen. |
| if (!entered_fullscreen) { |
| return; |
| } |
| // IMPORTANT: Store the web contents pointer in a temporary because |this| is |
| // deleted after ShowInterstitial(). |
| content::WebContents* contents = web_contents(); |
| ShowInterstitial(DelayedWarningEvent::kWarningShownOnFullscreenAttempt); |
| // Exit fullscreen only after navigating to the interstitial. We don't want to |
| // interfere with an ongoing fullscreen request. |
| contents->ExitFullscreen(will_cause_resize); |
| // DO NOT add code past this point. |this| is destroyed. |
| } |
| |
| void SafeBrowsingUserInteractionObserver::OnPaste() { |
| ShowInterstitial(DelayedWarningEvent::kWarningShownOnPaste); |
| // DO NOT add code past this point. |this| is destroyed. |
| } |
| |
| void SafeBrowsingUserInteractionObserver::OnBubbleAdded() { |
| // The page requested a permission that triggered a permission prompt. Deny |
| // and show the interstitial. |
| permissions::PermissionRequestManager* permission_request_manager = |
| permissions::PermissionRequestManager::FromWebContents(web_contents()); |
| if (!permission_request_manager) { |
| return; |
| } |
| permission_request_manager->Deny(); |
| ShowInterstitial(DelayedWarningEvent::kWarningShownOnPermissionRequest); |
| // DO NOT add code past this point. |this| is destroyed. |
| } |
| |
| void SafeBrowsingUserInteractionObserver::OnJavaScriptDialog() { |
| ShowInterstitial(DelayedWarningEvent::kWarningShownOnJavaScriptDialog); |
| // DO NOT add code past this point. |this| is destroyed. |
| } |
| |
| void SafeBrowsingUserInteractionObserver::OnPasswordSaveOrAutofillDenied() { |
| if (password_save_or_autofill_denied_metric_recorded_) { |
| return; |
| } |
| password_save_or_autofill_denied_metric_recorded_ = true; |
| RecordUMA(DelayedWarningEvent::kPasswordSaveOrAutofillDenied); |
| } |
| |
| void SafeBrowsingUserInteractionObserver::OnDesktopCaptureRequest() { |
| ShowInterstitial(DelayedWarningEvent::kWarningShownOnDesktopCaptureRequest); |
| // DO NOT add code past this point. |this| is destroyed. |
| } |
| |
| // static |
| void SafeBrowsingUserInteractionObserver:: |
| SetSuspiciousSiteReporterExtensionIdForTesting(const char* extension_id) { |
| suspicious_site_reporter_extension_id_ = extension_id; |
| } |
| |
| // static |
| void SafeBrowsingUserInteractionObserver:: |
| ResetSuspiciousSiteReporterExtensionIdForTesting() { |
| suspicious_site_reporter_extension_id_ = kPreventElisionExtensionId; |
| } |
| |
| void SafeBrowsingUserInteractionObserver::SetClockForTesting( |
| base::Clock* clock) { |
| clock_ = clock; |
| } |
| |
| base::Time SafeBrowsingUserInteractionObserver::GetCreationTimeForTesting() |
| const { |
| return creation_time_; |
| } |
| |
| void SafeBrowsingUserInteractionObserver::RecordUMA(DelayedWarningEvent event) { |
| Profile* profile = |
| Profile::FromBrowserContext(web_contents()->GetBrowserContext()); |
| if (IsUrlElisionDisabled(profile, suspicious_site_reporter_extension_id_)) { |
| base::UmaHistogramEnumeration(kDelayedWarningsWithElisionDisabledHistogram, |
| event); |
| } else { |
| base::UmaHistogramEnumeration(kDelayedWarningsHistogram, event); |
| } |
| } |
| |
| bool IsAllowedModifier(const content::NativeWebKeyboardEvent& event) { |
| const int key_modifiers = |
| event.GetModifiers() & blink::WebInputEvent::kKeyModifiers; |
| // If the only modifier is shift, the user may be typing uppercase |
| // letters. |
| if (key_modifiers == WebInputEvent::kShiftKey) { |
| return event.windows_key_code == ui::VKEY_SHIFT; |
| } |
| // Disallow CTRL+C and CTRL+V. |
| if (key_modifiers == WebInputEvent::kControlKey && |
| (event.windows_key_code == ui::VKEY_C || |
| event.windows_key_code == ui::VKEY_V)) { |
| return false; |
| } |
| return key_modifiers != 0; |
| } |
| |
| bool SafeBrowsingUserInteractionObserver::HandleKeyPress( |
| const content::NativeWebKeyboardEvent& event) { |
| // Allow non-character keys such as ESC. These can be used to exit fullscreen, |
| // for example. |
| if (!event.IsCharacterKey() || event.is_browser_shortcut || |
| IsAllowedModifier(event)) { |
| return false; |
| } |
| ShowInterstitial(DelayedWarningEvent::kWarningShownOnKeypress); |
| // DO NOT add code past this point. |this| is destroyed. |
| return true; |
| } |
| |
| bool SafeBrowsingUserInteractionObserver::HandleMouseEvent( |
| const blink::WebMouseEvent& event) { |
| if (event.GetType() != blink::WebInputEvent::Type::kMouseDown) { |
| return false; |
| } |
| // If warning isn't enabled for mouse clicks, still record the first time when |
| // the user clicks. |
| if (!kDelayedWarningsEnableMouseClicks.Get()) { |
| if (!mouse_click_with_no_warning_recorded_) { |
| RecordUMA(DelayedWarningEvent::kWarningNotTriggeredOnMouseClick); |
| mouse_click_with_no_warning_recorded_ = true; |
| } |
| return false; |
| } |
| ShowInterstitial(DelayedWarningEvent::kWarningShownOnMouseClick); |
| // DO NOT add code past this point. |this| is destroyed. |
| return true; |
| } |
| |
| void SafeBrowsingUserInteractionObserver::ShowInterstitial( |
| DelayedWarningEvent event) { |
| // Show the interstitial. |
| DCHECK(!interstitial_shown_); |
| interstitial_shown_ = true; |
| CleanUp(); |
| RecordUMA(event); |
| ui_manager_->StartDisplayingBlockingPage(resource_); |
| Detach(); |
| // DO NOT add code past this point. |this| is destroyed. |
| } |
| |
| void SafeBrowsingUserInteractionObserver::CleanUp() { |
| content::RenderWidgetHost* widget = |
| web_contents_->GetMainFrame()->GetRenderWidgetHost(); |
| widget->RemoveKeyPressEventCallback(key_press_callback_); |
| widget->RemoveMouseEventCallback(mouse_event_callback_); |
| } |
| |
| } // namespace safe_browsing |