CVE-2026-11280
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcomponents/signin/ios/browser/account_consistency_service_unittest.mm |
modified |
Files Changed
components/signin/ios/browser/account_consistency_service.mmcomponents/signin/ios/browser/account_consistency_service_unittest.mmcomponents/signin/public/base/signin_switches.cccomponents/signin/public/base/signin_switches.h
Patch
From a269ec32d5e9601230e2d181d802bfa77a530fb8 Mon Sep 17 00:00:00 2001
From: Boris Sazonov <bsazonov@chromium.org>
Date: Fri, 24 Apr 2026 11:32:33 -0700
Subject: [PATCH] [Signin][iOS] Ignore X-ChromeManageAccounts header in subframes
Ignore X-ChromeManageAccounts if received in a non-main frame.
Gated by kIgnoreChromeManageAccountsInSubframes killswitch.
Bug: 505200733, 501892820
Change-Id: I1af663eb267611727fbdf299c3f9ca929479ffaa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7786116
Commit-Queue: Boris Sazonov <bsazonov@chromium.org>
Reviewed-by: Jérôme Lebel <jlebel@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1620357}
---
diff --git a/components/signin/ios/browser/account_consistency_service.mm b/components/signin/ios/browser/account_consistency_service.mm
index 93a5376..a0acff70 100644
--- a/components/signin/ios/browser/account_consistency_service.mm
+++ b/components/signin/ios/browser/account_consistency_service.mm
@@ -22,6 +22,7 @@
#import "components/signin/core/browser/chrome_connected_header_helper.h"
#import "components/signin/core/browser/signin_header_helper.h"
#import "components/signin/ios/browser/features.h"
+#import "components/signin/public/base/signin_switches.h"
#import "components/signin/public/identity_manager/accounts_cookie_mutator.h"
#import "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
#import "google_apis/gaia/gaia_constants.h"
@@ -225,6 +226,13 @@
return;
}
+ if (!response_info.for_main_frame &&
+ base::FeatureList::IsEnabled(
+ switches::kIgnoreChromeManageAccountsInSubframes)) {
+ std::move(callback).Run(PolicyDecision::Allow());
+ return;
+ }
+
GURL url = net::GURLWithNSURL(http_response.URL);
// User is showing intent to navigate to a Google-owned domain. Set GAIA and
// CHROME_CONNECTED cookies if the user is signed in (this is filtered in
diff --git a/components/signin/ios/browser/account_consistency_service_unittest.mm b/components/signin/ios/browser/account_consistency_service_unittest.mm
index 21dfbc3b..9df98b3 100644
--- a/components/signin/ios/browser/account_consistency_service_unittest.mm
+++ b/components/signin/ios/browser/account_consistency_service_unittest.mm
@@ -830,3 +830,27 @@
EXPECT_EQ(1, delegate_.add_account_call_count_);
EXPECT_EQ("test@gmail.com", delegate_.add_account_email_);
}
+
+// Tests that the X-Chrome-Manage-Accounts header is ignored in a subframe.
+TEST_F(AccountConsistencyServiceTest, ChromeManageAccountsIgnoredInSubframe) {
+ base::test::ScopedFeatureList feature_list(
+ switches::kIgnoreChromeManageAccountsInSubframes);
+
+ NSDictionary* headers =
+ [NSDictionary dictionaryWithObject:@"action=DEFAULT"
+ forKey:@"X-Chrome-Manage-Accounts"];
+ NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc]
+ initWithURL:[NSURL URLWithString:@"https://accounts.google.com/"]
+ statusCode:200
+ HTTPVersion:@"HTTP/1.1"
+ headerFields:headers];
+
+ SetWebStateHandler(&delegate_);
+
+ // When feature is enabled, header is ignored and response is allowed.
+ EXPECT_TRUE(web_state_.ShouldAllowResponse(response,
+ /* for_main_frame = */ false));
+ web_state_.SetCurrentURL(net::GURLWithNSURL(response.URL));
+ web_state_.OnPageLoaded(web::PageLoadCompletionStatus::SUCCESS);
+ EXPECT_EQ(0, delegate_.total_call_count());
+}
diff --git a/components/signin/public/base/signin_switches.cc b/components/signin/public/base/signin_switches.cc
index c0b4bbd..5efa90f 100644
--- a/components/signin/public/base/signin_switches.cc
+++ b/components/signin/public/base/signin_switches.cc
@@ -512,6 +512,9 @@
#endif
#if BUILDFLAG(IS_IOS)
+BASE_FEATURE(kIgnoreChromeManageAccountsInSubframes,
+ base::FEATURE_ENABLED_BY_DEFAULT);
+
// Feature flag to ignore invalid grant errors in AuthenticationService.
BASE_FEATURE(kIgnoreInvalidGrantError, base::FEATURE_DISABLED_BY_DEFAULT);
#endif
diff --git a/components/signin/public/base/signin_switches.h b/components/signin/public/base/signin_switches.h
index 80aebb0..486418e 100644
--- a/components/signin/public/base/signin_switches.h
+++ b/components/signin/public/base/signin_switches.h
@@ -471,6 +471,10 @@
#endif
#if BUILDFLAG(IS_IOS)
+// Killswitch for ignoring X-Chrome-Manage-Accounts header in subframes.
+COMPONENT_EXPORT(SIGNIN_SWITCHES)
+BASE_DECLARE_FEATURE(kIgnoreChromeManageAccountsInSubframes);
+
// Feature flag to ignore invalid grant errors in AuthenticationService.
COMPONENT_EXPORT(SIGNIN_SWITCHES)
BASE_DECLARE_FEATURE(kIgnoreInvalidGrantError);
Regression Test / PoC
diff --git a/components/signin/ios/browser/account_consistency_service_unittest.mm b/components/signin/ios/browser/account_consistency_service_unittest.mm
index 21dfbc3b..9df98b3 100644
--- a/components/signin/ios/browser/account_consistency_service_unittest.mm
+++ b/components/signin/ios/browser/account_consistency_service_unittest.mm
@@ -830,3 +830,27 @@
EXPECT_EQ(1, delegate_.add_account_call_count_);
EXPECT_EQ("test@gmail.com", delegate_.add_account_email_);
}
+
+// Tests that the X-Chrome-Manage-Accounts header is ignored in a subframe.
+TEST_F(AccountConsistencyServiceTest, ChromeManageAccountsIgnoredInSubframe) {
+ base::test::ScopedFeatureList feature_list(
+ switches::kIgnoreChromeManageAccountsInSubframes);
+
+ NSDictionary* headers =
+ [NSDictionary dictionaryWithObject:@"action=DEFAULT"
+ forKey:@"X-Chrome-Manage-Accounts"];
+ NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc]
+ initWithURL:[NSURL URLWithString:@"https://accounts.google.com/"]
+ statusCode:200
+ HTTPVersion:@"HTTP/1.1"
+ headerFields:headers];
+
+ SetWebStateHandler(&delegate_);
+
+ // When feature is enabled, header is ignored and response is allowed.
+ EXPECT_TRUE(web_state_.ShouldAllowResponse(response,
+ /* for_main_frame = */ false));
+ web_state_.SetCurrentURL(net::GURLWithNSURL(response.URL));
+ web_state_.OnPageLoaded(web::PageLoadCompletionStatus::SUCCESS);
+ EXPECT_EQ(0, delegate_.total_call_count());
+}
Original Bug Report
Potential UI spoofing and forced actions via X-Chrome-Manage-Accounts header on iOS
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: Chrome for iOS’s AccountConsistencyHandler processes the X-Chrome-Manage-Accounts header from network responses without verifying the request initiator or main frame status. This lack of validation potentially allows malicious web pages to trigger native Chrome UI actions, such as opening incognito tabs or showing the ‘Add Account’ sheet. This parity gap with Desktop Chrome could be leveraged for UI spoofing or session disruption.
Affected files:
components/signin/ios/browser/account_consistency_service.mmios/chrome/browser/signin/model/account_consistency_browser_agent.mm
Estimated timestamp from git blame: 2025-08-22
Vulnerability Description
Chrome for iOS handles account consistency through AccountConsistencyHandler::ShouldAllowResponse, which monitors network responses from Gaia domains for the X-Chrome-Manage-Accounts HTTP header. If present, it extracts the requested action (e.g., INCOGNITO, ADDSESSION, SIGNOUT) and triggers native iOS UI components.
However, the iOS implementation in components/signin/ios/browser/account_consistency_service.mm does not validate the context of the response:
- It ignores the
response_info.for_main_frameparameter provided by theweb::WebStatePolicyDeciderAPI. - It lacks a mechanism to verify the
request_initiatorof the navigation.
On Desktop (chrome/browser/signin/chrome_signin_helper.cc), ProcessMirrorResponseHeaderIfExists explicitly enforces IsOutermostMainFrame() and restricts sensitive actions to trusted Google/YouTube initiators. Due to this parity gap, an attacker can embed a Gaia URL within an iframe on a malicious site or trigger an uncommitted top-level navigation, causing Chrome for iOS to process the resulting Mirror header and execute native actions against the user’s will.
Impact
By forcing the browser to process specific Mirror actions, an attacker can trigger unauthorized state changes:
- UI Spoofing / Phishing: Using
action=ADDSESSION, the attacker can force the native “Add Account” sheet to appear over the malicious site, potentially tricking the user into entering credentials in a confusing context. - Session Disruption: Using
action=INCOGNITO, the attacker forces Chrome to abruptly navigate the active tab backwards in its history (viaWebNavigationBrowserAgent::GoBack) and open a new Incognito tab.
Potential Reproduction Steps
Note: These are suggested steps based on static code analysis, as our tooling agent does not have the ability to run active code.
- A signed-in Chrome for iOS user visits
https://attacker.com. - The attacker’s page silently initiates a request (e.g., via a hidden
<iframe>orwindow.location.href) to a Gaia endpoint onaccounts.google.comconfigured to return theX-Chrome-Manage-Accountsheader with an action likeaction=INCOGNITO. AccountConsistencyHandler::ShouldAllowResponsereceives the response. It correctly verifies the Gaia URL but ignores that the request originated from an untrusted site and/or a subframe.- The handler extracts the action and invokes the corresponding delegate, such as
AccountConsistencyBrowserAgent::OnGoIncognito. - The browser forcibly opens a new incognito tab, completely bypassing user intent.
- Finally,
ShouldAllowResponsecancels the original Gaia navigation viaPolicyDecision::Cancel(), leaving the underlyingWebStatesituated on the attacker’s context while the native disruptions occur.
Suggested Fix
- Update
AccountConsistencyHandler::ShouldAllowResponseto immediately ignore the header if!response_info.for_main_frame. - Introduce a check for the
request_initiator(or equivalent context parameter) to ensure that sensitive Mirror actions are only processed if the request originated from a trusted Google domain, matching the security posture of the Desktop implementation.
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.