CVE-2026-106188
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/android/signin/java/src/org/chromium/chrome/browser/ui/signin/account_picker/WebSigninAccountPickerDelegate.java |
modified |
Files Changed
chrome/android/javatests/src/org/chromium/chrome/browser/signin/services/WebSigninLoadingDialogTest.javachrome/browser/ui/android/signin/java/src/org/chromium/chrome/browser/ui/signin/account_picker/WebSigninAccountPickerDelegate.javachrome/browser/ui/android/signin/junit/src/org/chromium/chrome/browser/ui/signin/account_picker/WebSigninAccountPickerDelegateTest.javachrome/android/java/src/org/chromium/chrome/browser/signin/WebSigninRedirectCoordinator.java
Patch
From 6be501d3a7b18c5dfc6b88d33050aabf44c6d56e Mon Sep 17 00:00:00 2001
From: Liza Bipin <mlbipin@google.com>
Date: Wed, 09 Sep 2026 03:16:10 -0700
Subject: [PATCH] Mark web sign-in continue URLs as renderer-initiated
Treat post-signin redirection navigations to the continue URL as
renderer-initiated rather than browser-initiated.
Fixed: 515477538
Change-Id: I078cd250cfdf1925e6e71e85b4ec0a20fc8d457b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8304472
Commit-Queue: Liza Bipin <mlbipin@google.com>
Reviewed-by: Boris Sazonov <bsazonov@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1694463}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/signin/WebSigninRedirectCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/signin/WebSigninRedirectCoordinator.java
index 7fe471b..bf5c1c0 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/signin/WebSigninRedirectCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/signin/WebSigninRedirectCoordinator.java
@@ -31,6 +31,7 @@
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.util.RunnableTimer;
import org.chromium.url.GURL;
+import org.chromium.url.Origin;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -360,7 +361,14 @@
recordWaitOutcomeCompleted();
destroy();
- mTab.loadUrl(new LoadUrlParams(mContinueUrl));
+ LoadUrlParams params = new LoadUrlParams(mContinueUrl);
+ // Mark the navigation as renderer-initiated rather than browser-initiated as the continue
+ // URL originates from the web page.
+ params.setIsRendererInitiated(true);
+ // Use an opaque origin because the source cannot be trusted and should not have same-origin
+ // privileges.
+ params.setInitiatorOrigin(Origin.createOpaqueOrigin());
+ mTab.loadUrl(params);
}
private void startTimerOrForceShowDialog() {
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/signin/services/WebSigninLoadingDialogTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/signin/services/WebSigninLoadingDialogTest.java
index 3ead9d4..7747e2a 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/signin/services/WebSigninLoadingDialogTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/signin/services/WebSigninLoadingDialogTest.java
@@ -393,6 +393,9 @@
ArgumentCaptor<LoadUrlParams> captor = ArgumentCaptor.forClass(LoadUrlParams.class);
verify(spyTab).loadUrl(captor.capture());
Assert.assertEquals("https://continue.url/", captor.getValue().getUrl());
+ Assert.assertTrue(captor.getValue().getIsRendererInitiated());
+ Assert.assertNotNull(captor.getValue().getInitiatorOrigin());
+ Assert.assertTrue(captor.getValue().getInitiatorOrigin().isOpaque());
}
@Test
diff --git a/chrome/browser/ui/android/signin/java/src/org/chromium/chrome/browser/ui/signin/account_picker/WebSigninAccountPickerDelegate.java b/chrome/browser/ui/android/signin/java/src/org/chromium/chrome/browser/ui/signin/account_picker/WebSigninAccountPickerDelegate.java
index a0b867e..d6b849c 100644
--- a/chrome/browser/ui/android/signin/java/src/org/chromium/chrome/browser/ui/signin/account_picker/WebSigninAccountPickerDelegate.java
+++ b/chrome/browser/ui/android/signin/java/src/org/chromium/chrome/browser/ui/signin/account_picker/WebSigninAccountPickerDelegate.java
@@ -25,6 +25,7 @@
import org.chromium.components.signin.metrics.SigninAccessPoint;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.url.GURL;
+import org.chromium.url.Origin;
import java.util.function.Function;
@@ -169,7 +170,7 @@
if (tab != null && !tab.isDestroyed()) {
// This code path may be called asynchronously, so check
// that the tab is still alive.
- tab.loadUrl(new LoadUrlParams(continueUrl));
+ tab.loadUrl(createLoadUrlParamsForContinueUrl(continueUrl));
}
SigninSurveyController.registerTrigger(
mProfile, SigninSurveyController.SigninSurveyType.WEB);
@@ -202,7 +203,7 @@
if (!tab.isDestroyed()) {
// This code path may be called asynchronously, so check that the tab is
// still alive.
- tab.loadUrl(new LoadUrlParams(continueUrl));
+ tab.loadUrl(createLoadUrlParamsForContinueUrl(continueUrl));
}
break;
case WebSigninTrackerResult.AUTH_ERROR:
@@ -221,6 +222,17 @@
};
}
+ private static LoadUrlParams createLoadUrlParamsForContinueUrl(GURL continueUrl) {
+ LoadUrlParams params = new LoadUrlParams(continueUrl);
+ // Mark the navigation as renderer-initiated rather than browser-initiated as the continue
+ // URL originates from the web page.
+ params.setIsRendererInitiated(true);
+ // Use an opaque origin because the source cannot be trusted and should not have same-origin
+ // privileges.
+ params.setInitiatorOrigin(Origin.createOpaqueOrigin());
+ return params;
+ }
+
private void destroyWebSigninBridge() {
if (mWebSigninBridge != null) {
mWebSigninBridge.destroy();
diff --git a/chrome/browser/ui/android/signin/junit/src/org/chromium/chrome/browser/ui/signin/account_picker/WebSigninAccountPickerDelegateTest.java b/chrome/browser/ui/android/signin/junit/src/org/chromium/chrome/browser/ui/signin/account_picker/WebSigninAccountPickerDelegateTest.java
index cc04e24..90b0690 100644
--- a/chrome/browser/ui/android/signin/junit/src/org/chromium/chrome/browser/ui/signin/account_picker/WebSigninAccountPickerDelegateTest.java
+++ b/chrome/browser/ui/android/signin/junit/src/org/chromium/chrome/browser/ui/signin/account_picker/WebSigninAccountPickerDelegateTest.java
@@ -199,6 +199,9 @@
LoadUrlParams loadUrlParams = mLoadUrlParamsCaptor.getValue();
Assert.assertEquals(
"Continue url does not match!", CONTINUE_URL.getSpec(), loadUrlParams.getUrl());
+ Assert.assertTrue(loadUrlParams.getIsRendererInitiated());
+ Assert.assertNotNull(loadUrlParams.getInitiatorOrigin());
+ Assert.assertTrue(loadUrlParams.getInitiatorOrigin().isOpaque());
} else if (result == WebSigninTrackerResult.OTHER_ERROR) {
verify(mSigninMetricsUtilsJniMock)
.logAccountConsistencyPromoAction(
Original Bug Report
Privilege escalation via unvalidated continue_url in X-Chrome-Manage-Accounts header
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The X-Chrome-Manage-Accounts (Mirror) header allows Google domains to trigger account management actions. When processing the ADDSESSION service type on Android, Chrome fails to validate the scheme of the provided continue_url, potentially allowing a malicious website to force a highly privileged browser-initiated navigation to internal chrome:// or intent:// URLs.
Affected files:
chrome/browser/signin/chrome_signin_helper.cccomponents/signin/core/browser/chrome_connected_header_helper.cc
Estimated timestamp from git blame: 2014-09-24
Overview
The X-Chrome-Manage-Accounts header is used by Google domains to signal account management actions to Chrome. When processing this header on Android, specifically for the GAIA_SERVICE_TYPE_ADDSESSION service type, Chrome does not validate the scheme of the continue_url parameter.
Because the resulting navigation is executed as browser-initiated (is_renderer_initiated = false), this lack of validation potentially allows an attacker to navigate the user’s browser to privileged chrome:// or intent:// URLs, bypassing web security boundaries.
Potential Exploitation Steps
Note: These steps are suggested based on static code analysis; an active Proof of Concept has not yet been executed.
- Attacker Lures User: An attacker hosts a malicious website (e.g.,
https://attacker.com/exploit.html) and lures a user to it on Chrome for Android. - Navigation to Google Endpoint: The attacker’s page executes a renderer-initiated top-level navigation to a specific Google endpoint that reflects the
continuequery parameter into theX-Chrome-Manage-Accountsheader. For example:window.location.href = "https://accounts.google.com/SignOutOptions?continue=chrome://flags". - Header Interception & Parsing: The
accounts.google.comserver responds with an HTTP redirect and the header:X-Chrome-Manage-Accounts: action=ADDSESSION,continue_url=chrome://flags. Chrome parses this header inChromeConnectedHeaderHelper::BuildManageAccountsParamsbut fails to validate the scheme of thecontinue_url. - Navigation Cancellation: The browser’s navigation stack blocks the
302redirect tochrome://flagsbecause it was initiated by web content. Consequently, the navigation fails to commit, and the tab’s visible URL remains the attacker’s page (https://attacker.com/exploit.html). - Task Execution (Missing Initiator Check): The asynchronously posted task
ProcessMirrorHeaderexecutes. WhileGAIA_SERVICE_TYPE_INCOGNITOincludes an initiator origin check,GAIA_SERVICE_TYPE_ADDSESSIONdoes not (noted by a TODO on line 246 ofchrome_signin_helper.cc). The code proceeds into theADDSESSIONflow. - Java Coordinator and Equality Check Bypass: The C++ code calls
SigninBridgeFactory::GetForProfile(profile)->WaitForCookiesAndRedirect(...), passing the unvalidatedchrome://flagsURL. This transitions to Java (SigninBridge.java), which records the current URL (mInitialTabURL). Because the previous navigation failed,mInitialTabURLremainshttps://attacker.com/exploit.html. - Browser-Initiated Navigation: After waiting for cookies,
WebSigninRedirectCoordinator#onSigninResultverifies thatmTab.getUrl().equals(mInitialTabURL). Since the URL never changed, this check passes. The code executesmTab.loadUrl(new LoadUrlParams(mContinueUrl)). By default,LoadUrlParamssetsmIsRendererInitiated = false. - Privileged Access: The navigation to
chrome://flags(or anintent://URI) succeeds because Chrome trusts browser-initiated navigations, resulting in a sandbox/security boundary escape.
Suggested Fix
- Scheme Validation: Update
ChromeConnectedHeaderHelper::BuildManageAccountsParamsorProcessMirrorHeaderto strictly validate the scheme of thecontinue_url. It should only permithttp://andhttps://schemes, strictly forbiddingchrome://,intent://,file://, etc. - Origin Initiator Check: Implement the missing initiator domain check for
GAIA_SERVICE_TYPE_ADDSESSIONinProcessMirrorHeader(resolving the TODO on line 246 inchrome/browser/signin/chrome_signin_helper.cc).
Evaluated with Chrome root at commit: a9e0d24757518a6f49f01bf5068f2cadfc6092f4
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.