CVE-2026-87519
Overview
Background
- Safe Browsing interstitial
- The full-page warning shown before an unsafe navigation, whose buttons and keyboard shortcuts emit
SecurityInterstitialCommandvalues such asCMD_PROCEEDandCMD_DONT_PROCEED. - `SafeBrowsingProceedAnywayDisabled` policy
- An enterprise policy that, when active, is surfaced through
is_proceed_anyway_disabled()and is meant to make the interstitial non-overridable so the user cannot continue to the unsafe site. - Keyboard override ("thisisunsafe")
- A hidden shortcut that dispatches a
CMD_PROCEEDcommand to the interstitial without the visible “proceed” button, per the patch description bypassing policy checks. - Allowlist
- A per-host record added in
base_blocking_page.ccon proceed so that Safe Browsing stops re-showing the interstitial for that host.
Root Cause Analysis
In the committed-interstitial proceed path in base_blocking_page.cc, the code added the unsafe host to the allowlist whenever the received command was CMD_PROCEED, with no check of whether proceeding was actually permitted. When the SafeBrowsingProceedAnywayDisabled policy was active, the “thisisunsafe” keyboard shortcut still emitted CMD_PROCEED, so the host was allowlisted before the policy could force the navigation back to safety — violating the invariant that a policy-blocked interstitial must never grant an authorization the user is forbidden to grant. In the error-UI command handlers (safe_browsing_loud_error_ui.cc and safe_browsing_quiet_error_ui.cc), user_made_decision_ = true was also set unconditionally on CMD_PROCEED (and the CMD_PROCEED case appears to fall through into CMD_DONT_PROCEED, per the added comment), further recording a decision that policy did not allow.
The fix gates the allowlist addition on !sb_error_ui_->is_proceed_anyway_disabled(), moves user_made_decision_ = true inside the !is_proceed_anyway_disabled() branch, and guards the CMD_DONT_PROCEED assignment against a fallthrough from a policy-blocked CMD_PROCEED. It also publishes disableKeyboardOverride to the front-end so the keyboard shortcut can be suppressed when proceeding is disabled.
CMD_PROCEED command as authorization to allowlist the host without consulting is_proceed_anyway_disabled(), so the keyboard override could allowlist a host the policy forbids proceeding to; the fix conditions both the allowlist write and the “user made a decision” bookkeeping on proceeding being permitted, and disables the keyboard override in the UI.Attack Path
- Policy makes the interstitial non-overridable
An administrator enables
SafeBrowsingProceedAnywayDisabled, sois_proceed_anyway_disabled()is true and the user is expected to be unable to continue to a flagged host. - User triggers the hidden keyboard override
On the Safe Browsing interstitial the user types the “thisisunsafe” shortcut, which dispatches a
CMD_PROCEEDcommand even though the visible proceed control is disabled (per the patch description). - Host is allowlisted before policy enforcement
In the pre-patch
base_blocking_page.ccproceed handler,CMD_PROCEEDcallsset_proceeded(true)and adds the host to the allowlist without checkingis_proceed_anyway_disabled(), so the allowlist entry is written prematurely. - Bypass persists past forced navigation
Policy checks then force navigation away, but the host has already been allowlisted, so the safety decision the policy meant to prevent has effectively been recorded — an incorrect authorization. Any subsequent access to that host succeeding without the interstitial is
(inferred), as the diff shows only the premature allowlist write, not later navigations.
Impact Assessment
SafeBrowsingProceedAnywayDisabled enforcement in the browser process, allowlisting a Safe-Browsing-flagged host despite a policy that should make the interstitial non-overridable. The precondition is that the policy is active and the user reaches the interstitial and issues the keyboard override; the advisory classifies this as incorrect authorization in Safe Browsing and does not report exploitation in the wild. Any consequence beyond the allowlisting itself — reaching malware/phishing content or further compromise — is (inferred), since the patch demonstrates only the premature allowlist write and the decision-recording bug.Changed Functions
| Function | Change | Notes |
|---|---|---|
switchcomponents/security_interstitials/core/safe_browsing_loud_error_ui.cc |
modified | |
ifcomponents/security_interstitials/core/safe_browsing_loud_error_ui.cc |
modified | |
switchcomponents/security_interstitials/core/safe_browsing_quiet_error_ui.cc |
modified |
Files Changed
components/safe_browsing/content/browser/ui_manager_unittest.cccomponents/security_interstitials/core/safe_browsing_loud_error_ui.cccomponents/security_interstitials/core/safe_browsing_quiet_error_ui.cccomponents/safe_browsing/content/browser/base_blocking_page.cc
Audit Directions
- Command-to-side-effect gatingAudit every interstitial
CMD_PROCEED/proceed handler for state writes (allowlist adds,set_proceeded,user_made_decision_) that fire on the command alone without consultingis_proceed_anyway_disabled()or the equivalent authorization flag. - Keyboard/alternate override entry pointsLook for shortcuts or programmatic paths (like “thisisunsafe”) that emit proceed commands independently of the visible UI, and verify they respect the same policy checks and the new
disableKeyboardOverridesignal. - Switch-case fallthrough into decision recordingReview command
switchstatements where a disabledCMD_PROCEEDfalls through toCMD_DONT_PROCEED, and confirm decision-recording assignments are guarded by the actual command rather than shared across cases.
Patch
From d592f083e77d44162c672637d66334cba1851561 Mon Sep 17 00:00:00 2001 From: Piotr Krzeszewski <[email protected]> Date: Thu, 06 Aug 2026 08:03:17 -0700 Subject: [PATCH] [Safe Browsing] Disable keyboard override when proceeding is disabled When SafeBrowsingProceedAnywayDisabled policy is active, typing "thisisunsafe" prematurely allowlisted the host before policy checks forced navigation away. This change: - Sets `disableKeyboardOverride` in SafeBrowsingLoudErrorUI. - Prevents allowlisting in BaseBlockingPage if proceeding is disabled. - Adds unit test coverage for policy enforcement. Tested with local reproduction steps presented in the bug Bug: 501763003 Change-Id: Idea6720c9a8c07c6bfb261e9d29d4e50734f84cc Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8178244 Reviewed-by: thefrog <[email protected]> Reviewed-by: Michał Kaczmarczyk <[email protected]> Commit-Queue: Piotr Krzeszewski <[email protected]> Reviewed-by: Dominique Fauteux-Chapleau <[email protected]> Cr-Commit-Position: refs/heads/main@{#1674980} --- diff --git a/components/safe_browsing/content/browser/base_blocking_page.cc b/components/safe_browsing/content/browser/base_blocking_page.cc index 4b07a2bc..d600077c 100644 --- a/components/safe_browsing/content/browser/base_blocking_page.cc +++ b/components/safe_browsing/content/browser/base_blocking_page.cc @@ -118,7 +118,8 @@ static_cast<security_interstitials::SecurityInterstitialCommand>(command); if (interstitial_command == - security_interstitials::SecurityInterstitialCommand::CMD_PROCEED) { + security_interstitials::SecurityInterstitialCommand::CMD_PROCEED && + !sb_error_ui_->is_proceed_anyway_disabled()) { // With committed interstitials, OnProceed() doesn't get called, so handle // adding to the allow list here. set_proceeded(true); diff --git a/components/security_interstitials/core/safe_browsing_loud_error_ui.cc b/components/security_interstitials/core/safe_browsing_loud_error_ui.cc index e12e759..a154c82 100644 --- a/components/security_interstitials/core/safe_browsing_loud_error_ui.cc +++ b/components/security_interstitials/core/safe_browsing_loud_error_ui.cc @@ -86,6 +86,7 @@ "primaryButtonText", l10n_util::GetStringUTF16(IDS_SAFEBROWSING_OVERRIDABLE_SAFETY_BUTTON)); load_time_data.Set("overridable", !is_proceed_anyway_disabled()); + load_time_data.Set("disableKeyboardOverride", is_proceed_anyway_disabled()); load_time_data.Set( security_interstitials::kOptInLink, l10n_util::GetStringUTF16(IDS_SAFE_BROWSING_SCOUT_REPORTING_AGREE)); @@ -131,8 +132,8 @@ switch (command) { case CMD_PROCEED: { // User pressed on the button to proceed. - user_made_decision_ = true; if (!is_proceed_anyway_disabled()) { + user_made_decision_ = true; controller()->metrics_helper()->RecordUserDecision( MetricsHelper::PROCEED); controller()->Proceed(); @@ -143,7 +144,11 @@ } case CMD_DONT_PROCEED: { // User pressed on the button to return to safety. - user_made_decision_ = true; + // Only record a user decision if the command was actually + // CMD_DONT_PROCEED (not a fallthrough from a policy-blocked CMD_PROCEED). + if (command == CMD_DONT_PROCEED) { + user_made_decision_ = true; + } // Don't record the user action here because there are other ways of // triggering DontProceed, like clicking the back button. if (is_main_frame_load_pending()) { diff --git a/components/security_interstitials/core/safe_browsing_quiet_error_ui.cc b/components/security_interstitials/core/safe_browsing_quiet_error_ui.cc index 75baec1..7482022 100644 --- a/components/security_interstitials/core/safe_browsing_quiet_error_ui.cc +++ b/components/security_interstitials/core/safe_browsing_quiet_error_ui.cc @@ -83,8 +83,8 @@ switch (command) { case CMD_PROCEED: { // User pressed on the button to proceed. - user_made_decision_ = true; if (!is_proceed_anyway_disabled()) { + user_made_decision_ = true; controller()->metrics_helper()->RecordUserDecision( MetricsHelper::PROCEED); controller()->Proceed(); diff --git a/components/safe_browsing/content/browser/ui_manager_unittest.cc b/components/safe_browsing/content/browser/ui_manager_unittest.cc index 4ef14a1..7ed70e7 100644 --- a/components/safe_browsing/content/browser/ui_manager_unittest.cc +++ b/components/safe_browsing/content/browser/ui_manager_unittest.cc @@ -96,7 +96,8 @@ TestSafeBrowsingBlockingPage(BaseUIManager* manager, content::WebContents* web_contents, const GURL& main_frame_url, - const UnsafeResourceList& unsafe_resources) + const UnsafeResourceList& unsafe_resources, + bool is_proceed_anyway_disabled = false) : SafeBrowsingBlockingPage( manager, web_contents, @@ -117,14 +118,14 @@ BaseSafeBrowsingErrorUI::SBErrorDisplayOptions( BaseBlockingPage::IsMainPageResourceLoadPending( unsafe_resources), - false, // is_extended_reporting_opt_in_allowed - false, // is_off_the_record - false, // is_extended_reporting_enabled - false, // is_extended_reporting_policy_managed - false, // is_enhanced_protection_enabled - false, // is_proceed_anyway_disabled - false, // should_open_links_in_new_tab - true, // always_show_back_to_safety + false, // is_extended_reporting_opt_in_allowed + false, // is_off_the_record + false, // is_extended_reporting_enabled + false, // is_extended_reporting_policy_managed + false, // is_enhanced_protection_enabled + is_proceed_anyway_disabled, // is_proceed_anyway_disabled + false, // should_open_links_in_new_tab + true, // always_show_back_to_safety false, // is_enhanced_protection_message_enabled false, // is_safe_browsing_managed "cpn_safe_browsing"), // help_center_article_link @@ -132,7 +133,7 @@ /*navigation_observer_manager=*/nullptr, /*metrics_collector=*/nullptr, /*trigger_manager=*/nullptr, - /*is_proceed_anyway_disabled=*/false, + is_proceed_anyway_disabled, /*is_safe_browsing_surveys_enabled=*/true, /*trust_safety_sentiment_service_trigger=*/base::NullCallback(), /*ignore_auto_revocation_notifications_trigger=*/ @@ -824,4 +825,27 @@ &threat_type)); } +TEST_F(SafeBrowsingUIManagerTest, + ProceedAnywayDisabled_CommandReceivedDoesNotAllowlist) { + security_interstitials::UnsafeResource resource = + MakeUnsafeResourceAndStartNavigation(kBadURL); + resource.threat_source = ThreatSource::LOCAL_PVER4; + std::vector<security_interstitials::UnsafeResource> resources = {resource}; + TestSafeBrowsingBlockingPage blocking_page( + ui_manager(), web_contents(), GURL(kBadURL), resources, + /*is_proceed_anyway_disabled=*/true); + + // 1. Verify frontend string population: disableKeyboardOverride must be true. + base::DictValue load_time_data; + blocking_page.sb_error_ui()->PopulateStringsForHtml(load_time_data); + std::optional<bool> disable_keyboard = + load_time_data.FindBool("disableKeyboardOverride"); + ASSERT_TRUE(disable_keyboard.has_value()); + EXPECT_TRUE(disable_keyboard.value()); + + // 2. Verify backend command handling: CMD_PROCEED (1) must not allowlist URL. + blocking_page.CommandReceived("1"); + EXPECT_FALSE(IsAllowlisted(resource)); +} + } // namespace safe_browsing
Regression Test / PoC
diff --git a/components/safe_browsing/content/browser/ui_manager_unittest.cc b/components/safe_browsing/content/browser/ui_manager_unittest.cc
index 4ef14a1..7ed70e7 100644
--- a/components/safe_browsing/content/browser/ui_manager_unittest.cc
+++ b/components/safe_browsing/content/browser/ui_manager_unittest.cc
@@ -96,7 +96,8 @@
TestSafeBrowsingBlockingPage(BaseUIManager* manager,
content::WebContents* web_contents,
const GURL& main_frame_url,
- const UnsafeResourceList& unsafe_resources)
+ const UnsafeResourceList& unsafe_resources,
+ bool is_proceed_anyway_disabled = false)
: SafeBrowsingBlockingPage(
manager,
web_contents,
@@ -117,14 +118,14 @@
BaseSafeBrowsingErrorUI::SBErrorDisplayOptions(
BaseBlockingPage::IsMainPageResourceLoadPending(
unsafe_resources),
- false, // is_extended_reporting_opt_in_allowed
- false, // is_off_the_record
- false, // is_extended_reporting_enabled
- false, // is_extended_reporting_policy_managed
- false, // is_enhanced_protection_enabled
- false, // is_proceed_anyway_disabled
- false, // should_open_links_in_new_tab
- true, // always_show_back_to_safety
+ false, // is_extended_reporting_opt_in_allowed
+ false, // is_off_the_record
+ false, // is_extended_reporting_enabled
+ false, // is_extended_reporting_policy_managed
+ false, // is_enhanced_protection_enabled
+ is_proceed_anyway_disabled, // is_proceed_anyway_disabled
+ false, // should_open_links_in_new_tab
+ true, // always_show_back_to_safety
false, // is_enhanced_protection_message_enabled
false, // is_safe_browsing_managed
"cpn_safe_browsing"), // help_center_article_link
@@ -132,7 +133,7 @@
/*navigation_observer_manager=*/nullptr,
/*metrics_collector=*/nullptr,
/*trigger_manager=*/nullptr,
- /*is_proceed_anyway_disabled=*/false,
+ is_proceed_anyway_disabled,
/*is_safe_browsing_surveys_enabled=*/true,
/*trust_safety_sentiment_service_trigger=*/base::NullCallback(),
/*ignore_auto_revocation_notifications_trigger=*/
@@ -824,4 +825,27 @@
&threat_type));
}
+TEST_F(SafeBrowsingUIManagerTest,
+ ProceedAnywayDisabled_CommandReceivedDoesNotAllowlist) {
+ security_interstitials::UnsafeResource resource =
+ MakeUnsafeResourceAndStartNavigation(kBadURL);
+ resource.threat_source = ThreatSource::LOCAL_PVER4;
+ std::vector<security_interstitials::UnsafeResource> resources = {resource};
+ TestSafeBrowsingBlockingPage blocking_page(
+ ui_manager(), web_contents(), GURL(kBadURL), resources,
+ /*is_proceed_anyway_disabled=*/true);
+
+ // 1. Verify frontend string population: disableKeyboardOverride must be true.
+ base::DictValue load_time_data;
+ blocking_page.sb_error_ui()->PopulateStringsForHtml(load_time_data);
+ std::optional<bool> disable_keyboard =
+ load_time_data.FindBool("disableKeyboardOverride");
+ ASSERT_TRUE(disable_keyboard.has_value());
+ EXPECT_TRUE(disable_keyboard.value());
+
+ // 2. Verify backend command handling: CMD_PROCEED (1) must not allowlist URL.
+ blocking_page.CommandReceived("1");
+ EXPECT_FALSE(IsAllowlisted(resource));
+}
+
} // namespace safe_browsing