CVE-2026-18008
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
testchrome/test/data/webui/cr_elements/cr_dialog_test.ts |
modified | |
testchrome/test/data/webui/settings/people_page_test.ts |
modified | |
ifui/webui/resources/cr_elements/cr_dialog/cr_dialog.ts |
modified |
Files Changed
chrome/test/data/webui/cr_elements/cr_dialog_test.tschrome/test/data/webui/settings/people_page_test.tsui/webui/resources/cr_elements/cr_dialog/cr_dialog.ts
Patch
From c75b91d96c91535c730d4be0ac4e34837500ac93 Mon Sep 17 00:00:00 2001 From: dpapad <[email protected]> Date: Tue, 16 Jun 2026 12:20:26 -0700 Subject: [PATCH] WebUI: Fix leaking cr-dialog 'popstate' listener when disconnected. The leaking listener is the root cause for various flaky tests, such as the one that caused the revert at crrev.com/c/7944686. After fixing the leaking listener, some workarounds in chrome/test/data/webui/settings/people_page_test.ts can be removed, where individual test cases were waiting for 'popstate' to fire before declaring the test done, to not interfere with subsequent tests, which is fragile and shouldn't be the responsibility of a test case to begin with. Bug: 522412676 Change-Id: Id72486ea8f4dd84be95839aaee6e28e3dd692dce Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7947646 Commit-Queue: Demetrios Papadopoulos <[email protected]> Reviewed-by: John Lee <[email protected]> Cr-Commit-Position: refs/heads/main@{#1647795} --- diff --git a/chrome/test/data/webui/cr_elements/cr_dialog_test.ts b/chrome/test/data/webui/cr_elements/cr_dialog_test.ts index df7d46eed..cdfbe4e0 100644 --- a/chrome/test/data/webui/cr_elements/cr_dialog_test.ts +++ b/chrome/test/data/webui/cr_elements/cr_dialog_test.ts @@ -616,4 +616,22 @@ window.dispatchEvent(new CustomEvent('popstate')); assertTrue(dialog.open); }); + + test('popstate listener removed on disconnect', function() { + document.body.innerHTML = getTrustedHTML` + <cr-dialog> + <div slot="title">title</div> + </cr-dialog>`; + const dialog = document.body.querySelector('cr-dialog')!; + dialog.showModal(); + + let cancelFired = false; + dialog.addEventListener('cancel', () => { + cancelFired = true; + }); + + dialog.remove(); + window.dispatchEvent(new CustomEvent('popstate')); + assertFalse(cancelFired); + }); }); diff --git a/chrome/test/data/webui/settings/people_page_test.ts b/chrome/test/data/webui/settings/people_page_test.ts index 506bd691..2f551ac0 100644 --- a/chrome/test/data/webui/settings/people_page_test.ts +++ b/chrome/test/data/webui/settings/people_page_test.ts @@ -380,12 +380,6 @@ loadTimeData.getStringF( 'deleteProfileWarningWithCountsPlural', 2, 'fakeUsername'), warningMessage.textContent.trim()); - - // Close the disconnect dialog. - signoutDialog.$.disconnectConfirm.click(); - await new Promise(function(resolve) { - listenOnce(window, 'popstate', resolve); - }); }); test('NavigateDirectlyToSignOutURL', async function() { @@ -401,13 +395,6 @@ // handler if the user navigates directly to // chrome://settings/signOut. if so, it should not cause a crash. new ProfileInfoBrowserProxyImpl().getProfileStatsCount(); - - // Close the disconnect dialog. - peoplePage.shadowRoot!.querySelector('settings-signout-dialog')!.$ - .disconnectConfirm.click(); - await new Promise(function(resolve) { - listenOnce(window, 'popstate', resolve); - }); }); test('Signout dialog suppressed when not signed in', async function() { diff --git a/ui/webui/resources/cr_elements/cr_dialog/cr_dialog.ts b/ui/webui/resources/cr_elements/cr_dialog/cr_dialog.ts index 2fb3122f..91f8a25 100644 --- a/ui/webui/resources/cr_elements/cr_dialog/cr_dialog.ts +++ b/ui/webui/resources/cr_elements/cr_dialog/cr_dialog.ts @@ -22,6 +22,7 @@ import '../cr_icon_button/cr_icon_button.js'; import {assert} from '//resources/js/assert.js'; +import {EventTracker} from '//resources/js/event_tracker.js'; import {CrLitElement} from '//resources/lit/v3_0/lit.rollup.js'; import type {CrInputElement} from '../cr_input/cr_input.js'; @@ -108,6 +109,7 @@ private mutationObserver_: MutationObserver|null = null; private boundKeydown_: ((e: KeyboardEvent) => void)|null = null; + private tracker_: EventTracker = new EventTracker(); override connectedCallback() { super.connectedCallback(); @@ -131,6 +133,12 @@ if (this.showOnAttach) { this.showModal(); } + + this.tracker_.add(window, 'popstate', () => { + if (!this.ignorePopstate && this.$.dialog.open) { + this.cancel(); + } + }); } override disconnectedCallback() { @@ -140,17 +148,10 @@ this.mutationObserver_.disconnect(); this.mutationObserver_ = null; } + this.tracker_.removeAll(); } override firstUpdated() { - // If the active history entry changes (i.e. user clicks back button), - // all open dialogs should be cancelled. - window.addEventListener('popstate', () => { - if (!this.ignorePopstate && this.$.dialog.open) { - this.cancel(); - } - }); - if (!this.ignoreEnterKey) { this.addEventListener('keypress', this.onKeypress_.bind(this)); }
Regression Test / PoC
diff --git a/chrome/test/data/webui/cr_elements/cr_dialog_test.ts b/chrome/test/data/webui/cr_elements/cr_dialog_test.ts
index df7d46eed..cdfbe4e0 100644
--- a/chrome/test/data/webui/cr_elements/cr_dialog_test.ts
+++ b/chrome/test/data/webui/cr_elements/cr_dialog_test.ts
@@ -616,4 +616,22 @@
window.dispatchEvent(new CustomEvent('popstate'));
assertTrue(dialog.open);
});
+
+ test('popstate listener removed on disconnect', function() {
+ document.body.innerHTML = getTrustedHTML`
+ <cr-dialog>
+ <div slot="title">title</div>
+ </cr-dialog>`;
+ const dialog = document.body.querySelector('cr-dialog')!;
+ dialog.showModal();
+
+ let cancelFired = false;
+ dialog.addEventListener('cancel', () => {
+ cancelFired = true;
+ });
+
+ dialog.remove();
+ window.dispatchEvent(new CustomEvent('popstate'));
+ assertFalse(cancelFired);
+ });
});
diff --git a/chrome/test/data/webui/settings/people_page_test.ts b/chrome/test/data/webui/settings/people_page_test.ts
index 506bd691..2f551ac0 100644
--- a/chrome/test/data/webui/settings/people_page_test.ts
+++ b/chrome/test/data/webui/settings/people_page_test.ts
@@ -380,12 +380,6 @@
loadTimeData.getStringF(
'deleteProfileWarningWithCountsPlural', 2, 'fakeUsername'),
warningMessage.textContent.trim());
-
- // Close the disconnect dialog.
- signoutDialog.$.disconnectConfirm.click();
- await new Promise(function(resolve) {
- listenOnce(window, 'popstate', resolve);
- });
});
test('NavigateDirectlyToSignOutURL', async function() {
@@ -401,13 +395,6 @@
// handler if the user navigates directly to
// chrome://settings/signOut. if so, it should not cause a crash.
new ProfileInfoBrowserProxyImpl().getProfileStatsCount();
-
- // Close the disconnect dialog.
- peoplePage.shadowRoot!.querySelector('settings-signout-dialog')!.$
- .disconnectConfirm.click();
- await new Promise(function(resolve) {
- listenOnce(window, 'popstate', resolve);
- });
});
test('Signout dialog suppressed when not signed in', async function() {
Original Bug Report
HTML Link Injection in chrome://settings Sign-Out Dialog via Account Domain
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential HTML/link injection vulnerability exists in the chrome://settings sign-out confirmation dialog on non-ChromeOS desktop platforms. The dialog’s helper function interpolates the account domain name verbatim into an HTML template block without escaping, which is then bound to the DOM using inner-h-t-m-l after basic subset sanitization. An attacker who compromises the network process could forge a user profile response to inject arbitrary hyperlinks into this trusted settings UI surface.
Affected files:
chrome/browser/resources/settings/people_page/signout_dialog.tschrome/browser/resources/settings/people_page/signout_dialog.html
Estimated timestamp from git blame: 2016-06-29
Description
A potential HTML link injection vulnerability has been identified in the chrome://settings sign-out confirmation dialog. Due to a lack of escaping on the user’s primary account email domain name, unescaped HTML tags can be interpolated directly into a localized UI string and subsequently bound to the DOM.
Root Cause
In chrome/browser/resources/settings/people_page/signout_dialog.ts (lines 132-136), the getDisconnectExplanationHtml_ function constructs an HTML fragment to display the domain name managing the current account:
private getDisconnectExplanationHtml_(domain: string): TrustedHTML {
if (domain) {
return sanitizeInnerHtml(loadTimeData.getStringF(
'syncDisconnectManagedProfileExplanation', `<span>${domain}</span>`));
}
...
}
This fragment is rendered via Polymer’s inner-h-t-m-l in chrome/browser/resources/settings/people_page/signout_dialog.html (lines 24-26):
<div inner-h-t-m-l="[[
getDisconnectExplanationHtml_(syncStatus.domain)]]">
</div>
The syncStatus.domain property originates from the browser process backend in PeopleHandler::GetSyncStatusDictionary() (chrome/browser/ui/webui/settings/people_handler.cc):
if (enterprise_util::UserAcceptedAccountManagement(profile_) &&
!primary_account_info.email.empty()) {
sync_status.Set("domain",
gaia::ExtractDomainName(primary_account_info.email));
}
gaia::ExtractDomainName in google_apis/gaia/gaia_auth_util.cc extracts the substring following the @ separator in the canonicalized email address. Crucially, the domain component is not sanitized or stripped of HTML characters, undergoing only simple ASCII lowercasing.
While the WebUI’s sanitizeInnerHtml uses parseHtmlSubset to prevent script execution, its static allowlist permits SPAN and A elements. It also allows href attributes starting with https:// or chrome:// schemes. An attacker with the ability to inject custom HTML inside the domain name parameter can therefore introduce fully functional hyperlinks and arbitrary styling tags into the trusted Settings UI.
Potential Attack Scenario
(Note: These are potential steps as our tooling does not currently have the environment or capabilities to run and execute a live Proof-of-Concept exploit)
- An attacker compromises the network process or performs a position-in-the-middle attack on the Gaia endpoint.
- When the browser retrieves user profile info via
/userinfo, the attacker returns a response containing a forged email payload containing HTML tags, such as:"email": "[email protected]</span><a href=https://attacker.example>confirm sign-out here</a><span>" - The browser process ingests this email address verbatim and stores it in the account tracker service.
- When the user accepted account management conditions navigates to
chrome://settingsand clicks to turn off sync or sign out, the backend extracts the domain:evil.com</span><a href=https://attacker.example>confirm sign-out here</a><span>. - The sign-out dialog renders, executing
getDisconnectExplanationHtml_and injecting the attacker’s<a>tag with itshttps://hyperlink into the trusted Settings dialog.
Suggested Remediation
To address this vulnerability, the domain string must be properly HTML-escaped before it is interpolated into the HTML template fragment.
Import the standard htmlEscape helper in chrome/browser/resources/settings/people_page/signout_dialog.ts and wrap the domain parameter before interpolation:
import {htmlEscape} from 'chrome://resources/js/util.js';
private getDisconnectExplanationHtml_(domain: string): TrustedHTML {
if (domain) {
return sanitizeInnerHtml(loadTimeData.getStringF(
'syncDisconnectManagedProfileExplanation', `<span>${htmlEscape(domain)}</span>`));
}
...
}
Evaluated with Chrome root at commit: b2fea2e31df308d0f04e4ae47def4c4f939ee141
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.