Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Signin
DescriptionInsufficient validation of untrusted input in Signin
ComponentSignin
Bug ClassLogic Error
Tracker501892820
Fix commita269ec32d5e9 (chromium/src) +39/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST_F
components/signin/ios/browser/account_consistency_service_unittest.mm
modified

Files Changed

  • components/signin/ios/browser/account_consistency_service.mm
  • components/signin/ios/browser/account_consistency_service_unittest.mm
  • components/signin/public/base/signin_switches.cc
  • components/signin/public/base/signin_switches.h
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);
Loading diff…

Regression Test / PoC

shipped with the fix
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());
+}
Loading diff…

Original Bug Report

reported by vm...@google.com

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.mm
  • ios/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:

  1. It ignores the response_info.for_main_frame parameter provided by the web::WebStatePolicyDecider API.
  2. It lacks a mechanism to verify the request_initiator of 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 (via WebNavigationBrowserAgent::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.

  1. A signed-in Chrome for iOS user visits https://attacker.com.
  2. The attacker’s page silently initiates a request (e.g., via a hidden <iframe> or window.location.href) to a Gaia endpoint on accounts.google.com configured to return the X-Chrome-Manage-Accounts header with an action like action=INCOGNITO.
  3. AccountConsistencyHandler::ShouldAllowResponse receives the response. It correctly verifies the Gaia URL but ignores that the request originated from an untrusted site and/or a subframe.
  4. The handler extracts the action and invokes the corresponding delegate, such as AccountConsistencyBrowserAgent::OnGoIncognito.
  5. The browser forcibly opens a new incognito tab, completely bypassing user intent.
  6. Finally, ShouldAllowResponse cancels the original Gaia navigation via PolicyDecision::Cancel(), leaving the underlying WebState situated on the attacker’s context while the native disruptions occur.

Suggested Fix

  1. Update AccountConsistencyHandler::ShouldAllowResponse to immediately ignore the header if !response_info.for_main_frame.
  2. 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.

View on issue tracker
Links in the report