CVE-2026-106408
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper_unittest.mm |
modified |
Files Changed
ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.mmios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper_unittest.mmios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.h
Patch
From 33312408f3550ed5840bdbcf24cc1b92a988f383 Mon Sep 17 00:00:00 2001 From: Alexandra Pereira <[email protected]> Date: Thu, 10 Sep 2026 14:19:01 -0700 Subject: [PATCH] [iOS] Upgrade HTTP redirect targets from POST navigations HttpsOnlyModeUpgradeTabHelper caches whether a navigation is a POST in DidStartNavigation() to exempt form submissions from HTTPS upgrades. However, HTTP server redirects (e.g. 302/303) convert POST requests to GETs. Because navigation_is_post_ was not updated, ShouldAllowRequest() continued to treat the redirect as a POST, allowing cleartext HTTP to load without an upgrade or interstitial. Update navigation_is_post_ in ShouldAllowRequest() using the incoming request's HTTPMethod so that redirected GET requests are properly upgraded. Fixed: 504227656 Change-Id: I939e1f018a614cca18988c70ec8f5bf86600beec Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8382522 Reviewed-by: Mustafa Emre Acer <[email protected]> Commit-Queue: Alexandra Pereira <[email protected]> Cr-Commit-Position: refs/heads/main@{#1695669} --- diff --git a/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.h b/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.h index 1ed3bb4..1156440e 100644 --- a/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.h +++ b/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.h @@ -115,9 +115,10 @@ ui::PageTransition navigation_transition_type_ = ui::PAGE_TRANSITION_FIRST; bool navigation_is_renderer_initiated_ = false; web::Referrer referrer_; - // Set to true when a new navigation with a POST method is started. - // Used to check if the navigation should be upgraded when a response is - // received. Cleared when the current navigation finishes. + // Set to true when a new navigation with a POST method is started + // and updated during redirects (e.g. when a 3xx redirect converts a + // POST to a GET). Used to check if the navigation should be upgraded + // when a response is received. Cleared when the current navigation finishes. bool navigation_is_post_ = false; // Set to true when a main frame navigation has started but not yet finished. // Used to distinguish ShouldAllowRequest calls for redirects within an diff --git a/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.mm b/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.mm index 98a1bd6..52d9107 100644 --- a/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.mm +++ b/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.mm @@ -159,6 +159,8 @@ return; } + navigation_is_post_ = [request.HTTPMethod isEqualToString:@"POST"]; + if ((!base::FeatureList::IsEnabled( security_interstitials::features::kHttpsUpgrades) && !(prefs_ && prefs_->GetBoolean(prefs::kHttpsOnlyModeEnabled))) || diff --git a/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper_unittest.mm b/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper_unittest.mm index 5ffac52..7bd18a28 100644 --- a/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper_unittest.mm +++ b/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper_unittest.mm @@ -266,6 +266,36 @@ } } +// Tests that when a POST navigation redirects to HTTP, ShouldAllowRequest +// cancels the HTTP redirect when HTTPS-Only Mode or HTTPS-Upgrades is enabled. +TEST_P(HttpsOnlyModeUpgradeTabHelperTest, + ShouldAllowRequestPostRedirectToHttp) { + auto fake_navigation_manager = std::make_unique<web::FakeNavigationManager>(); + std::unique_ptr<web::NavigationItem> pending_item = + web::NavigationItem::Create(); + fake_navigation_manager->SetPendingItem(pending_item.release()); + web_state_.SetNavigationManager(std::move(fake_navigation_manager)); + + GURL https_url("https://example.com/"); + GURL http_url("http://example.com/"); + + // Start an HTTPS POST navigation. + web::FakeNavigationContext context; + context.SetUrl(https_url); + context.SetIsPost(true); + web_state_.OnNavigationStarted(&context); + + // If either HTTPS-Only Mode or HTTPS-Upgrades is enabled, redirects to HTTP + // in the main frame should be cancelled before the request is sent. + if (GetParam() != HttpsUpgradesTestType::kNone) { + EXPECT_FALSE(ShouldAllowRequestUrl(http_url, /*main_frame=*/true) + .ShouldAllowNavigation()); + } else { + EXPECT_TRUE(ShouldAllowRequestUrl(http_url, /*main_frame=*/true) + .ShouldAllowNavigation()); + } +} + TEST_P(HttpsOnlyModeUpgradeTabHelperTest, GetUpgradedHttpsUrl) { ProfileIOS* profile = ProfileIOS::FromBrowserState(web_state_.GetBrowserState());
Regression Test / PoC
diff --git a/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper_unittest.mm b/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper_unittest.mm
index 5ffac52..7bd18a28 100644
--- a/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper_unittest.mm
+++ b/ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper_unittest.mm
@@ -266,6 +266,36 @@
}
}
+// Tests that when a POST navigation redirects to HTTP, ShouldAllowRequest
+// cancels the HTTP redirect when HTTPS-Only Mode or HTTPS-Upgrades is enabled.
+TEST_P(HttpsOnlyModeUpgradeTabHelperTest,
+ ShouldAllowRequestPostRedirectToHttp) {
+ auto fake_navigation_manager = std::make_unique<web::FakeNavigationManager>();
+ std::unique_ptr<web::NavigationItem> pending_item =
+ web::NavigationItem::Create();
+ fake_navigation_manager->SetPendingItem(pending_item.release());
+ web_state_.SetNavigationManager(std::move(fake_navigation_manager));
+
+ GURL https_url("https://example.com/");
+ GURL http_url("http://example.com/");
+
+ // Start an HTTPS POST navigation.
+ web::FakeNavigationContext context;
+ context.SetUrl(https_url);
+ context.SetIsPost(true);
+ web_state_.OnNavigationStarted(&context);
+
+ // If either HTTPS-Only Mode or HTTPS-Upgrades is enabled, redirects to HTTP
+ // in the main frame should be cancelled before the request is sent.
+ if (GetParam() != HttpsUpgradesTestType::kNone) {
+ EXPECT_FALSE(ShouldAllowRequestUrl(http_url, /*main_frame=*/true)
+ .ShouldAllowNavigation());
+ } else {
+ EXPECT_TRUE(ShouldAllowRequestUrl(http_url, /*main_frame=*/true)
+ .ShouldAllowNavigation());
+ }
+}
+
TEST_P(HttpsOnlyModeUpgradeTabHelperTest, GetUpgradedHttpsUrl) {
ProfileIOS* profile =
ProfileIOS::FromBrowserState(web_state_.GetBrowserState());
Original Bug Report
HTTPS-First Mode bypass on iOS via POST-to-GET redirect
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. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: A logic error in HttpsOnlyModeUpgradeTabHelper on iOS allows a navigation to bypass the HTTPS-First Mode interstitial when a POST request redirects to an HTTP URL. This occurs because the tab helper captures the initial HTTP method but fails to update its state to reflect that the navigation method changed to GET during the redirect.
Affected files:
ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.mmios/web/navigation/crw_wk_navigation_handler.mm
Estimated timestamp from git blame: 2025-07-04
Description
The HttpsOnlyModeUpgradeTabHelper on iOS contains a logic flaw that leads to a potential bypass of the “Always Use Secure Connections” (HTTPS-First Mode) setting. The bypass occurs when a navigation involves a server-side redirect from an HTTPS POST request to an insecure HTTP URL.
HTTPS-First Mode intentionally exempts POST requests from being upgraded to HTTPS to avoid the technical complexities and risks of silently re-transmitting POST data over a new connection. This exemption is tracked by the navigation_is_post_ boolean flag within the tab helper.
However, this flag is captured once during DidStartNavigation and is never refreshed:
// ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.mm
void HttpsOnlyModeUpgradeTabHelper::DidStartNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
// ...
if (state_ == State::kNone) {
// Store navigation parameters on initial navigation.
// ...
navigation_is_post_ = navigation_context->IsPost();
}
}
In a standard POST-redirect-GET scenario, the server responds to the POST request with an HTTP 302 or 303 redirect. The underlying WKWebView handles this by changing the request method to GET and navigating to the redirect target. CRWWKNavigationHandler catches this in webView:didReceiveServerRedirectForProvisionalNavigation: and notifies observers via DidRedirectNavigation.
Because HttpsOnlyModeUpgradeTabHelper does not implement DidRedirectNavigation, it misses this notification. Its internal navigation_is_post_ flag remains true.
When the insecure target HTTP server responds, ShouldAllowResponse is invoked. The logic to trigger an HTTPS upgrade or show the security interstitial relies on the stale navigation_is_post_ flag:
// ios/chrome/browser/https_upgrades/model/https_only_mode_upgrade_tab_helper.mm:211
if (item_pending->GetHttpsUpgradeType() == web::HttpsUpgradeType::kNone &&
!navigation_is_post_) {
// ... logic to trigger StopToUpgrade() ...
}
Because navigation_is_post_ evaluates to true, the upgrade logic is skipped. Execution falls through to line 252, explicitly allowing the insecure HTTP response without any interstitial.
Potential Reproduction Steps
- On an iOS device, enable Settings > Privacy > Always Use Secure Connections.
- Set up an attacker-controlled HTTPS server (
https://attacker.example) that auto-submits an HTTP POST form to an endpoint on the same server (https://attacker.example/redirect). - Configure the
/redirectendpoint to respond with an HTTP 302 Found redirect to an insecure HTTP URL (e.g.,http://victim.example/). - Visit
https://attacker.example. - Observe that
http://victim.example/loads successfully over an insecure HTTP connection, bypassing the expected HTTPS-First Mode interstitial.
Suggested Fix
Implement web::WebStateObserver::DidRedirectNavigation in HttpsOnlyModeUpgradeTabHelper to update the navigation_is_post_ flag when the HTTP method changes during a redirect:
void HttpsOnlyModeUpgradeTabHelper::DidRedirectNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
navigation_is_post_ = navigation_context->IsPost();
}
Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646
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.