CVE-2026-18009
Overview
Files Changed
chrome/browser/resources/password_manager/passwords_section.ts
Patch
From 4d64a6d4934ec705db0d98974e9e85e172e14baf Mon Sep 17 00:00:00 2001
From: Andrii Natiahlyi <natiahlyi@google.com>
Date: Thu, 11 Jun 2026 05:18:46 -0700
Subject: [PATCH] Escape account email in empty passwords section state.
Fixed: 522419718
Change-Id: I0e2372a4f7c6390d20f00cf64834fe1cbd9b0177
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7922494
Auto-Submit: Andrii Natiahlyi <natiahlyi@google.com>
Commit-Queue: Andrii Natiahlyi <natiahlyi@google.com>
Commit-Queue: Adem Derinel <derinel@google.com>
Reviewed-by: Adem Derinel <derinel@google.com>
Cr-Commit-Position: refs/heads/main@{#1645284}
---
diff --git a/chrome/browser/resources/password_manager/passwords_section.ts b/chrome/browser/resources/password_manager/passwords_section.ts
index 593e7a77..2f39d3d2 100644
--- a/chrome/browser/resources/password_manager/passwords_section.ts
+++ b/chrome/browser/resources/password_manager/passwords_section.ts
@@ -20,6 +20,7 @@
import {focusWithoutInk} from 'chrome://resources/js/focus_without_ink.js';
import {sanitizeInnerHtml} from 'chrome://resources/js/parse_html_subset.js';
import {PluralStringProxyImpl} from 'chrome://resources/js/plural_string_proxy.js';
+import {htmlEscape} from 'chrome://resources/js/util.js';
import type {IronListElement} from 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
@@ -295,7 +296,7 @@
return this.i18nAdvanced('emptyStateImportSyncing', {
substitutions: [
this.i18n('localPasswordManager'),
- this.accountEmail,
+ htmlEscape(this.accountEmail),
],
});
}
Original Bug Report
HTML and Link Injection in chrome://password-manager via Unescaped Account Email
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 and link injection vulnerability exists in chrome://password-manager because the primary account email is rendered unescaped in an i18nAdvanced string template. A compromised network process can supply a maliciously crafted email address containing allowlisted markup, allowing an attacker to inject arbitrary links and spoof the user interface. This injected link can bypass standard navigation restriction handlers to open external phishing pages directly from the trusted WebUI.
Affected files:
chrome/browser/resources/password_manager/passwords_section.tschrome/browser/resources/password_manager/passwords_section.html
Estimated timestamp from git blame: 2023-04-21
Root Cause Analysis
In chrome/browser/resources/password_manager/passwords_section.ts (lines 290-303), the method computeImportPasswordsText_() constructs the empty-state invitation text for syncing users:
private computeImportPasswordsText_(): TrustedHTML {
if (this.isAccountStoreUser) {
return this.i18nAdvanced('emptyStateImportAccountStore');
}
if (this.isSyncingPasswords) {
return this.i18nAdvanced('emptyStateImportSyncing', {
substitutions: [
this.i18n('localPasswordManager'),
this.accountEmail,
],
});
}
return this.i18nAdvanced('emptyStateImportDevice');
}
The accountEmail property is supplied directly as a raw substitution parameter to this.i18nAdvanced without any prior sanitization or HTML escaping (unlike other WebUI dialogs in the same directory, which use htmlEscape).
The resulting TrustedHTML is bound directly to the Polymer inner-h-t-m-l attribute of the #importPasswords div in chrome/browser/resources/password_manager/passwords_section.html (lines 45-48):
<div id='importPasswords' class='cr-secondary-text'
hidden='[[!showImportPasswordsOption_(groups_, passwordManagerDisabled_)]]'
inner-h-t-m-l='[[importPasswordsText_]]'>
</div>
i18nAdvanced delegates to sanitizeInnerHtml and parseHtmlSubset (ui/webui/resources/js/parse_html_subset.ts). The default element/attribute allowlist in parseHtmlSubset permits standard anchor elements (<a>) containing href values starting with https:// / # and target='_blank'.
Additionally, the click listener on #importPasswords is registered in updateImportPasswordsLink_() using querySelector('a') to only grab the first anchor element:
private updateImportPasswordsLink_() {
const importLink = this.$.importPasswords.querySelector('a');
assert(importLink);
importLink.addEventListener('click', (event: Event) => {
event.preventDefault();
...
});
}
Because the translation template IDS_PASSWORD_MANAGER_UI_EMPTY_STATE_SYNCING_USERS places the $2 (email) placeholder before the legitimate ‘select a CSV file’ anchor link, prepending a dummy anchor like <a href='#'></a> within the injected email ensures the preventDefault() listener is attached to the dummy link, allowing secondary attacker-injected links to retain default navigation behavior and open external URLs when clicked.
Suggested Attack Flow (Potential Steps)
Note: These are potential steps; our tooling does not yet have the capability to execute code in a live environment.
- A compromised network process intercepts requests to Google’s Gaia OAuth
/userinfoendpoint for the signed-in primary account. - The network process overrides the JSON body of the response to keep the legitimate GAIA ID/subject but returns a crafted email address containing allowlisted markup:
'email': '<a href=\'#\'></a><a href=\'https://attacker.example/verify\' target=\'_blank\'><b>Verify your Google account</b></a>' - The browser process receives and stores this email string verbatim within
AccountTrackerService/AccountInfo(components/signin/internal/identity_manager/account_tracker_service.cc:299). - The
SyncHandlerpropagates this updated email to thechrome://password-managerWebUI viastored-accounts-changed(chrome/browser/ui/webui/password_manager/sync_handler.cc:263). - When a password-syncing user with an empty credential vault navigates to
chrome://password-manager,computeImportPasswordsText_()interpolates the raw payload. - The parser preserves the anchor tag, and the WebUI renders the fully styled, trusted-looking link inside the privileged page.
- When the user clicks ‘Verify your Google account’, the page navigates directly to
https://attacker.examplein a new tab.
Suggested Remediation
To remediate this potential vulnerability, the account email must be HTML-escaped before being supplied as a substitution parameter to i18nAdvanced.
Import htmlEscape from chrome://resources/js/util.js and wrap this.accountEmail inside computeImportPasswordsText_():
import {htmlEscape} from 'chrome://resources/js/util.js';
// ...
private computeImportPasswordsText_(): TrustedHTML {
if (this.isAccountStoreUser) {
return this.i18nAdvanced('emptyStateImportAccountStore');
}
if (this.isSyncingPasswords) {
return this.i18nAdvanced('emptyStateImportSyncing', {
substitutions: [
this.i18n('localPasswordManager'),
htmlEscape(this.accountEmail), // <-- Safe substitution
],
});
}
return this.i18nAdvanced('emptyStateImportDevice');
}
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.