CVE-2026-13894
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
HttpsUpgradesAdvancedProtectionBrowserTestchrome/browser/ssl/https_upgrades_browsertest.cc |
modified | |
HttpsUpgradesAdvancedProtectionBrowserTestchrome/browser/ssl/https_upgrades_browsertest.cc |
modified |
Files Changed
chrome/browser/ssl/https_upgrades_browsertest.ccchrome/browser/ssl/https_upgrades_interceptor.cc
Patch
From 46126f97d261427dbf9b70c833b3ac1976e32cf1 Mon Sep 17 00:00:00 2001
From: Chris Thompson <cthomp@chromium.org>
Date: Thu, 07 May 2026 15:08:20 -0700
Subject: [PATCH] Fix ABH bypass for Advanced Protection users
Some ABH Balanced Mode exemptions were incorrectly applying to Advanced
Protection users after previous changes to how Advanced Protection
status was applied (in crbug.com/480099712). This fixes this by checking
IsInterstitialEnabled() and IsStrictInterstitialEnabled() instead of
querying the underlying pref value in various places (throttle creation
and when checking exemption status). This also simplifies setup by
consolidating logic into ComputeInterstitialState().
This also adds a regression test (using the non-default ports
exemption).
Bug: 501741117
Change-Id: Ib2dc4d446ae08cb1e6b090334a66470a49d77fba
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7824656
Reviewed-by: Mustafa Emre Acer <meacer@chromium.org>
Commit-Queue: Chris Thompson <cthomp@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1627271}
---
diff --git a/chrome/browser/ssl/https_upgrades_browsertest.cc b/chrome/browser/ssl/https_upgrades_browsertest.cc
index 193d92dd..93ad2f6 100644
--- a/chrome/browser/ssl/https_upgrades_browsertest.cc
+++ b/chrome/browser/ssl/https_upgrades_browsertest.cc
@@ -20,6 +20,8 @@
#include "chrome/browser/interstitials/security_interstitial_page_test_utils.h"
#include "chrome/browser/preloading/scoped_prewarm_feature_list.h"
#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/safe_browsing/advanced_protection_status_manager.h"
+#include "chrome/browser/safe_browsing/advanced_protection_status_manager_factory.h"
#include "chrome/browser/ssl/chrome_security_blocking_page_factory.h"
#include "chrome/browser/ssl/generated_https_first_mode_pref.h"
#include "chrome/browser/ssl/https_first_mode_settings_tracker.h"
@@ -300,6 +302,7 @@
features::kHttpsFirstModeV2ForTypicallySecureUsers,
features::kHttpsFirstBalancedMode,
features::kHttpsFirstBalancedModeAutoEnable,
+ features::kHttpsFirstModeForAdvancedProtectionUsers,
security_interstitials::features::kHttpsFirstDialogUi});
break;
}
@@ -466,6 +469,13 @@
https_upgrades_test_type() == HttpsUpgradesTestType::kAll;
}
+ // Whether HFM strict mode is enabled (via pref).
+ // TODO: crbug.com/510847675 - Extend this to also include Advanced
+ // Protection.
+ bool IsStrictInterstitialEnabledForTest() const {
+ return IsHttpsFirstModePrefEnabled();
+ }
+
// Whether HFM is enabled for many sites, and thus the tests should run steps
// that assume the HTTP interstitial will trigger (i.e., for fallback HTTP
// navigations when HTTPS-First Mode is enabled).
@@ -787,7 +797,7 @@
auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents();
- if (IsHttpsFirstModePrefEnabled()) {
+ if (IsStrictInterstitialEnabledForTest()) {
// HFM should attempt the upgrade, fail, and fallback to the interstitial.
EXPECT_FALSE(content::NavigateToURL(contents, local_ip_url));
EXPECT_TRUE(
@@ -834,7 +844,7 @@
auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents();
- if (IsHttpsFirstModePrefEnabled()) {
+ if (IsStrictInterstitialEnabledForTest()) {
// HFM should attempt the upgrade, fail, and fallback to the interstitial.
EXPECT_FALSE(content::NavigateToURL(contents, singlelabel_url));
EXPECT_TRUE(
@@ -856,7 +866,7 @@
// If in Strict Mode, verify that upgrade events were recorded because an
// upgrade was attempted and failed.
- if (IsHttpsFirstModePrefEnabled()) {
+ if (IsStrictInterstitialEnabledForTest()) {
histograms()->ExpectTotalCount(kEventHistogram, 3);
histograms()->ExpectBucketCount(
kEventHistogram,
@@ -888,7 +898,7 @@
// wouldn't receive the traffic (since it relies on DNS).
auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents();
- if (IsHttpsFirstModePrefEnabled()) {
+ if (IsStrictInterstitialEnabledForTest()) {
EXPECT_FALSE(content::NavigateToURL(contents, nonunique_url1));
EXPECT_FALSE(content::NavigateToURL(contents, nonunique_url2));
// Other histograms are still recorded.
@@ -928,7 +938,7 @@
auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents();
- if (IsHttpsFirstModePrefEnabled() || IsIncognito()) {
+ if (IsStrictInterstitialEnabledForTest()) {
// HFM should attempt the upgrade, fail, and fallback to the interstitial.
EXPECT_FALSE(content::NavigateToURL(contents, non_default_http_url));
EXPECT_TRUE(
@@ -948,9 +958,9 @@
histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 1);
}
- // If in Strict Mode or Incognito, verify that upgrade events were recorded
+ // If in Strict Mode, verify that upgrade events were recorded
// because an upgrade was attempted and failed.
- if (IsHttpsFirstModePrefEnabled() || IsIncognito()) {
+ if (IsStrictInterstitialEnabledForTest()) {
histograms()->ExpectTotalCount(kEventHistogram, 3);
histograms()->ExpectBucketCount(
kEventHistogram,
@@ -3533,7 +3543,7 @@
AutocompleteMatch());
nav_observer.Wait();
- if (IsHttpsFirstModePrefEnabled() || IsIncognito()) {
+ if (IsStrictInterstitialEnabledForTest()) {
// Typed http URLs don't opt out of upgrades in HFM.
EXPECT_EQ(https_url, contents->GetLastCommittedURL());
} else {
@@ -4168,3 +4178,57 @@
EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(
contents));
}
+
+// A separate test fixture for Advanced Protection to ensure that HFM triggers
+// properly, even if balanced mode is enabled by default.
+class HttpsUpgradesAdvancedProtectionBrowserTest : public InProcessBrowserTest {
+ public:
+ HttpsUpgradesAdvancedProtectionBrowserTest() {
+ feature_list_.InitWithFeatures(
+ /*enabled_features=*/{features::
+ kHttpsFirstModeForAdvancedProtectionUsers,
+ features::kHttpsFirstBalancedModeAutoEnable},
+ /*disabled_features=*/{
+ security_interstitials::features::kHttpsFirstDialogUi});
+ }
+ ~HttpsUpgradesAdvancedProtectionBrowserTest() override = default;
+
+ void SetUp() override {
+ ChromeSecurityBlockingPageFactory::SetEnterpriseManagedForTesting(false);
+ InProcessBrowserTest::SetUp();
+ }
+
+ void SetUpOnMainThread() override {
+ // Enable Advanced Protection for the profile via the testing API
+ safe_browsing::AdvancedProtectionStatusManagerFactory::GetForProfile(
+ browser()->profile())
+ ->SetAdvancedProtectionStatusForTesting(true);
+
+ host_resolver()->AddRule("*", "127.0.0.1");
+ http_server_.AddDefaultHandlers(GetChromeTestDataDir());
+ ASSERT_TRUE(http_server_.Start());
+ }
+
+ net::EmbeddedTestServer* http_server() { return &http_server_; }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+ net::EmbeddedTestServer http_server_{net::EmbeddedTestServer::TYPE_HTTP};
+};
+
+// Verifies that Advanced Protection users get HFM warnings for non-default
+// ports and it does not get incorrectly bypassed by the Balanced Mode
+// exclusion. Regression test for crbug.com/501741117.
+IN_PROC_BROWSER_TEST_F(HttpsUpgradesAdvancedProtectionBrowserTest,
+ UrlWithNonDefaultPort_ShouldUpgradeAndShowInterstitial) {
+ GURL http_url = http_server()->GetURL("foo.com", "/simple.html");
+ EXPECT_NE(http_url.IntPort(), 80); // Ensure it's not the default port
+
+ auto* contents = browser()->tab_strip_model()->GetActiveWebContents();
+
+ content::TestNavigationObserver nav_observer(contents, 1);
+ EXPECT_FALSE(content::NavigateToURL(contents, http_url));
+ nav_observer.Wait();
+ EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(
+ contents));
+}
diff --git a/chrome/browser/ssl/https_upgrades_interceptor.cc b/chrome/browser/ssl/https_upgrades_interceptor.cc
index a73bd85..1261d27 100644
--- a/chrome/browser/ssl/https_upgrades_interceptor.cc
+++ b/chrome/browser/ssl/https_upgrades_interceptor.cc
@@ -201,20 +201,14 @@
return nullptr;
}
- PrefService* prefs = profile->GetPrefs();
- bool https_first_mode_enabled =
- prefs && prefs->GetBoolean(prefs::kHttpsOnlyModeEnabled);
-
- return std::make_unique<HttpsUpgradesInterceptor>(
- frame_tree_node_id, https_first_mode_enabled, navigation_ui_data);
+ return std::make_unique<HttpsUpgradesInterceptor>(frame_tree_node_id,
+ navigation_ui_data);
}
Regression Test / PoC
diff --git a/chrome/browser/ssl/https_upgrades_browsertest.cc b/chrome/browser/ssl/https_upgrades_browsertest.cc
index 193d92dd..93ad2f6 100644
--- a/chrome/browser/ssl/https_upgrades_browsertest.cc
+++ b/chrome/browser/ssl/https_upgrades_browsertest.cc
@@ -20,6 +20,8 @@
#include "chrome/browser/interstitials/security_interstitial_page_test_utils.h"
#include "chrome/browser/preloading/scoped_prewarm_feature_list.h"
#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/safe_browsing/advanced_protection_status_manager.h"
+#include "chrome/browser/safe_browsing/advanced_protection_status_manager_factory.h"
#include "chrome/browser/ssl/chrome_security_blocking_page_factory.h"
#include "chrome/browser/ssl/generated_https_first_mode_pref.h"
#include "chrome/browser/ssl/https_first_mode_settings_tracker.h"
@@ -300,6 +302,7 @@
features::kHttpsFirstModeV2ForTypicallySecureUsers,
features::kHttpsFirstBalancedMode,
features::kHttpsFirstBalancedModeAutoEnable,
+ features::kHttpsFirstModeForAdvancedProtectionUsers,
security_interstitials::features::kHttpsFirstDialogUi});
break;
}
@@ -466,6 +469,13 @@
https_upgrades_test_type() == HttpsUpgradesTestType::kAll;
}
+ // Whether HFM strict mode is enabled (via pref).
+ // TODO: crbug.com/510847675 - Extend this to also include Advanced
+ // Protection.
+ bool IsStrictInterstitialEnabledForTest() const {
+ return IsHttpsFirstModePrefEnabled();
+ }
+
// Whether HFM is enabled for many sites, and thus the tests should run steps
// that assume the HTTP interstitial will trigger (i.e., for fallback HTTP
// navigations when HTTPS-First Mode is enabled).
@@ -787,7 +797,7 @@
auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents();
- if (IsHttpsFirstModePrefEnabled()) {
+ if (IsStrictInterstitialEnabledForTest()) {
// HFM should attempt the upgrade, fail, and fallback to the interstitial.
EXPECT_FALSE(content::NavigateToURL(contents, local_ip_url));
EXPECT_TRUE(
@@ -834,7 +844,7 @@
auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents();
- if (IsHttpsFirstModePrefEnabled()) {
+ if (IsStrictInterstitialEnabledForTest()) {
// HFM should attempt the upgrade, fail, and fallback to the interstitial.
EXPECT_FALSE(content::NavigateToURL(contents, singlelabel_url));
EXPECT_TRUE(
@@ -856,7 +866,7 @@
// If in Strict Mode, verify that upgrade events were recorded because an
// upgrade was attempted and failed.
- if (IsHttpsFirstModePrefEnabled()) {
+ if (IsStrictInterstitialEnabledForTest()) {
histograms()->ExpectTotalCount(kEventHistogram, 3);
histograms()->ExpectBucketCount(
kEventHistogram,
@@ -888,7 +898,7 @@
// wouldn't receive the traffic (since it relies on DNS).
auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents();
- if (IsHttpsFirstModePrefEnabled()) {
+ if (IsStrictInterstitialEnabledForTest()) {
EXPECT_FALSE(content::NavigateToURL(contents, nonunique_url1));
EXPECT_FALSE(content::NavigateToURL(contents, nonunique_url2));
// Other histograms are still recorded.
@@ -928,7 +938,7 @@
auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents();
- if (IsHttpsFirstModePrefEnabled() || IsIncognito()) {
+ if (IsStrictInterstitialEnabledForTest()) {
// HFM should attempt the upgrade, fail, and fallback to the interstitial.
EXPECT_FALSE(content::NavigateToURL(contents, non_default_http_url));
EXPECT_TRUE(
@@ -948,9 +958,9 @@
histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 1);
}
- // If in Strict Mode or Incognito, verify that upgrade events were recorded
+ // If in Strict Mode, verify that upgrade events were recorded
// because an upgrade was attempted and failed.
- if (IsHttpsFirstModePrefEnabled() || IsIncognito()) {
+ if (IsStrictInterstitialEnabledForTest()) {
histograms()->ExpectTotalCount(kEventHistogram, 3);
histograms()->ExpectBucketCount(
kEventHistogram,
@@ -3533,7 +3543,7 @@
AutocompleteMatch());
nav_observer.Wait();
- if (IsHttpsFirstModePrefEnabled() || IsIncognito()) {
+ if (IsStrictInterstitialEnabledForTest()) {
// Typed http URLs don't opt out of upgrades in HFM.
EXPECT_EQ(https_url, contents->GetLastCommittedURL());
} else {
@@ -4168,3 +4178,57 @@
EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(
contents));
}
+
+// A separate test fixture for Advanced Protection to ensure that HFM triggers
+// properly, even if balanced mode is enabled by default.
+class HttpsUpgradesAdvancedProtectionBrowserTest : public InProcessBrowserTest {
+ public:
+ HttpsUpgradesAdvancedProtectionBrowserTest() {
+ feature_list_.InitWithFeatures(
+ /*enabled_features=*/{features::
+ kHttpsFirstModeForAdvancedProtectionUsers,
+ features::kHttpsFirstBalancedModeAutoEnable},
+ /*disabled_features=*/{
+ security_interstitials::features::kHttpsFirstDialogUi});
+ }
+ ~HttpsUpgradesAdvancedProtectionBrowserTest() override = default;
+
+ void SetUp() override {
+ ChromeSecurityBlockingPageFactory::SetEnterpriseManagedForTesting(false);
+ InProcessBrowserTest::SetUp();
+ }
+
+ void SetUpOnMainThread() override {
+ // Enable Advanced Protection for the profile via the testing API
+ safe_browsing::AdvancedProtectionStatusManagerFactory::GetForProfile(
+ browser()->profile())
+ ->SetAdvancedProtectionStatusForTesting(true);
+
+ host_resolver()->AddRule("*", "127.0.0.1");
+ http_server_.AddDefaultHandlers(GetChromeTestDataDir());
+ ASSERT_TRUE(http_server_.Start());
+ }
+
+ net::EmbeddedTestServer* http_server() { return &http_server_; }
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
+ net::EmbeddedTestServer http_server_{net::EmbeddedTestServer::TYPE_HTTP};
+};
+
+// Verifies that Advanced Protection users get HFM warnings for non-default
+// ports and it does not get incorrectly bypassed by the Balanced Mode
+// exclusion. Regression test for crbug.com/501741117.
+IN_PROC_BROWSER_TEST_F(HttpsUpgradesAdvancedProtectionBrowserTest,
+ UrlWithNonDefaultPort_ShouldUpgradeAndShowInterstitial) {
+ GURL http_url = http_server()->GetURL("foo.com", "/simple.html");
+ EXPECT_NE(http_url.IntPort(), 80); // Ensure it's not the default port
+
+ auto* contents = browser()->tab_strip_model()->GetActiveWebContents();
+
+ content::TestNavigationObserver nav_observer(contents, 1);
+ EXPECT_FALSE(content::NavigateToURL(contents, http_url));
+ nav_observer.Wait();
+ EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(
+ contents));
+}
Original Bug Report
HTTPS-First Mode bypass for Advanced Protection users
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.
Overview: The HttpsUpgradesInterceptor fails to check Advanced Protection (AP) status when manually constructing its interstitial state. Consequently, AP users silently bypass HTTPS-First Mode strict warnings for certain HTTP navigations, such as non-default ports or local IPs. This allows a network attacker to serve malicious plaintext payloads to users whose settings UI falsely indicates protections are enforced.
Affected files:
chrome/browser/ssl/https_upgrades_interceptor.ccchrome/browser/ssl/https_upgrades_util.ccchrome/browser/ssl/https_upgrades_navigation_throttle.cc
Estimated timestamp from git blame: 2024-10-22
Note: The following exploitation steps are suggested and potential. Our AI tooling agent does not currently have the ability to run or verify proof-of-concept code dynamically.
Root Cause
There is a potential logic flaw in chrome/browser/ssl/https_upgrades_interceptor.cc where HttpsUpgradesInterceptor::MaybeCreateLoader() manually populates an HttpInterstitialState struct to determine if HTTPS-First Mode (HFM) protections apply to a navigation.
While doing so, it sets the enabled_by_pref flag based on the user’s prefs::kHttpsOnlyModeEnabled setting, but it entirely omits querying the AdvancedProtectionStatusManager to set the enabled_by_advanced_protection flag.
Because the Advanced Protection status was decoupled from the kHttpsOnlyModeEnabled preference (via the kHttpsFirstModeForAdvancedProtectionUsers feature), an AP user’s enabled_by_pref will typically evaluate to false. Without the enabled_by_advanced_protection flag set, the interceptor incorrectly assesses that strict HFM rules do not apply to the current profile.
This causes the interceptor to incorrectly exempt specific categories of HTTP URLs from HTTPS upgrades and strict warnings, including:
- Non-default ports:
IsStrictInterstitialEnabled()evaluates tofalse, allowing HTTP requests to ports other than 80 to bypass upgrades. - Non-unique hostnames / Local IPs:
ShouldExemptNonUniqueHostnames()evaluates totrue, allowing plaintext navigations to RFC1918 IPs or single-label hosts. - Content Setting Exceptions: Upgrades are suppressed if a
MIXEDSCRIPT=ALLOWcontent setting is present because it only checks!interstitial_state_->enabled_by_pref.
Simultaneously, GeneratedHttpsFirstModePref correctly checks the AP status to display HFM as “Enforced by Advanced Protection” in the Chrome Settings UI, creating a direct security-UI mismatch.
Potential Attacker Steps
An attacker could exploit this under the following suggested sequence of events:
- The attacker gains Man-in-the-Middle (MitM) positioning on the local network of a user enrolled in Advanced Protection.
- The attacker tricks the user into clicking a link or otherwise navigating to an HTTP URL that falls into one of the exempted categories, such as a non-default port (e.g.,
http://example.com:8080/) or a local IP address. - The browser begins the navigation. In
MaybeCreateLoader(), theHttpsUpgradesInterceptorfails to recognize the user’s AP status. It treats the navigation as non-strict and explicitly exempts the URL from an HTTPS upgrade. - The HTTP request is passed to the network stack and transmitted in plaintext.
- The MitM attacker intercepts the request and responds with a malicious payload. The browser silently commits the HTTP response without ever showing the HTTPS-First Mode interstitial warning.
Suggested Fix
Update HttpsUpgradesInterceptor::MaybeCreateLoader() to correctly account for Advanced Protection status. Instead of manually constructing the HttpInterstitialState struct and missing fields, it should ideally use the existing ComputeInterstitialState() utility function defined in chrome/browser/ssl/https_upgrades_util.cc.
If manual construction is strictly required due to dependency constraints, the interceptor must be updated to query safe_browsing::AdvancedProtectionStatusManagerFactory::GetForProfile(profile) and set interstitial_state_->enabled_by_advanced_protection accordingly.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.