CVE-2026-11204
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 <[email protected]> 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 <[email protected]> Reviewed-by: Jérôme Lebel <[email protected]> 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("[email protected]", 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("[email protected]", 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 Privilege Escalation via Unvalidated continue_url in Account Consistency Flow on iOS
Flapjack, 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: The X-Chrome-Manage-Accounts header parser on iOS does not validate the scheme of the continue_url parameter and processes the header even in subframes. This allows a malicious web page framing a GAIA endpoint to force the browser to open privileged chrome:// URLs in a new tab by inadvertently bypassing navigation security checks.
Affected files:
components/signin/ios/browser/account_consistency_service.mmios/chrome/browser/signin/model/account_consistency_browser_agent.mmios/chrome/browser/url_loading/model/scene_url_loading_service.mm
Estimated timestamp from git blame: 2025-04-30
Description
There is a potential privilege escalation vulnerability in Chrome for iOS where an untrusted web page can force the browser to navigate to restricted chrome:// WebUIs.
This vulnerability relies on a confused deputy attack involving the X-Chrome-Manage-Accounts HTTP response header, which is sent by Google GAIA endpoints (e.g., accounts.google.com) to coordinate account management flows with the browser.
When a response containing this header is received, the network traffic is intercepted by AccountConsistencyService::AccountConsistencyHandler::ShouldAllowResponse (components/signin/ios/browser/account_consistency_service.mm:217). This handler processes the header if the origin is a legitimate GAIA domain.
However, the iOS implementation has two critical weaknesses:
- Lack of Main Frame Check: Unlike the desktop implementation, the iOS handler fails to check
response_info.for_main_frame. It processes the header even if the GAIA URL is loaded in a cross-origin<iframe>. - Lack of Scheme Validation: The header is parsed into
signin::ManageAccountsParams, and thecontinue_urlparameter is extracted. The code builds aGURLfrom this string but fails to validate that its scheme is a standard web scheme (e.g., HTTP/HTTPS).
If the action is INCOGNITO, the untrusted continue_url is passed directly to AccountConsistencyBrowserAgent::OnGoIncognito (ios/chrome/browser/signin/model/account_consistency_browser_agent.mm:213), which creates an OpenNewTabCommand.
The command is routed to SceneUrlLoadingService::LoadUrlInNewTab (ios/chrome/browser/url_loading/model/scene_url_loading_service.mm:26). To facilitate opening the new tab, this service unconditionally sets the navigation transition type to ui::PAGE_TRANSITION_TYPED.
Finally, when the navigation begins, CRWWKNavigationHandler::shouldAllowAppSpecificURLNavigationAction evaluates whether to allow the navigation to the chrome:// URL. Because the transition type was artificially forged to PAGE_TRANSITION_TYPED (simulating user omnibox input), the security check is bypassed, and the privileged WebUI (e.g., chrome://flags or chrome://prefs-internals) is loaded.
Potential Exploit Steps
(Note: These are suggested steps based on code analysis; our tooling agent does not yet have the ability to run code.)
- An attacker creates a malicious webpage (
evil.com). - The malicious page embeds a hidden
<iframe>pointing to a known GAIA endpoint that reflects URL parameters into theX-Chrome-Manage-Accountsheader, such as:https://accounts.google.com/SignOutOptions?continue=chrome://flags. - When the victim visits
evil.com, the iframe loads. Although GAIA’sX-Frame-Optionswill prevent the content from rendering, Chrome’sShouldAllowResponseintercepts the response headers beforehand. - Chrome parses the header, observes the
INCOGNITOaction, and extractschrome://flagsas thecontinue_url. - Chrome automatically navigates the current main tab back and opens a new incognito tab directly to
chrome://flags, bypassing navigation restrictions.
Recommended Fix
- Main Frame Validation: Modify
AccountConsistencyService::AccountConsistencyHandler::ShouldAllowResponseto checkresponse_info.for_main_frameand ignore theX-Chrome-Manage-Accountsheader if it arrives in a subframe, aligning with the desktop implementation. - Scheme Validation: Validate the
continue_urlparameter to ensure it uses a safe scheme (httporhttps) before dispatching the action to the delegate.
Evaluated with Chrome root at commit: 4a3e9db74111a3c6c4b3acfd70050a05077cf27a
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.