CVE-2026-13912
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mm |
modified | |
ifios/chrome/browser/safe_browsing/model/resources/password_protection.ts |
modified |
Files Changed
ios/chrome/browser/safe_browsing/model/password_protection_egtest.mmios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mmios/chrome/browser/safe_browsing/model/resources/password_protection.ts
Patch
From ff8491ed87d451863a19afbb7d78d490d6f957e2 Mon Sep 17 00:00:00 2001
From: Joshua Hood <jdh@chromium.org>
Date: Wed, 06 May 2026 07:26:26 -0700
Subject: [PATCH] [iOS] Fix SB keypress event listening
Bug: 508259433,504185807
Change-Id: I8dda45f2394fd4249f5a24d558e9ec4e0df6f42f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7818799
Reviewed-by: Daniel White <danieltwhite@google.com>
Commit-Queue: jdh <jdh@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1626163}
---
diff --git a/ios/chrome/browser/safe_browsing/model/password_protection_egtest.mm b/ios/chrome/browser/safe_browsing/model/password_protection_egtest.mm
index 70a58a9..09a66999 100644
--- a/ios/chrome/browser/safe_browsing/model/password_protection_egtest.mm
+++ b/ios/chrome/browser/safe_browsing/model/password_protection_egtest.mm
@@ -35,7 +35,21 @@
std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
const net::test_server::HttpRequest& request) {
auto http_response = std::make_unique<net::test_server::BasicHttpResponse>();
- http_response->set_content("Input: <input type='text' id='input'>");
+ if (request.relative_url.find("preventDefault=true") != std::string::npos) {
+ http_response->set_content(
+ "Input: <input type='text' id='input'>"
+ "<script>"
+ " document.getElementById('input').addEventListener('keydown', "
+ "function(e) {"
+ " e.preventDefault();"
+ " if (e.key.length === 1) {"
+ " document.getElementById('input').value += e.key;"
+ " }"
+ " });"
+ "</script>");
+ } else {
+ http_response->set_content("Input: <input type='text' id='input'>");
+ }
http_response->set_content_type("text/html");
return http_response;
}
@@ -61,7 +75,9 @@
std::string("--mark_as_allowlisted_for_phish_guard=") +
_allowlistedURL.spec());
- if ([self isRunningTest:@selector(testPasswordReuseDetectionWarning)]) {
+ if ([self isRunningTest:@selector(testPasswordReuseDetectionWarning)] ||
+ [self isRunningTest:@selector
+ (testPasswordReuseDetectionKeydownPreventDefault)]) {
// Use commandline args to save a fake phishing cached verdict.
config.additional_args.push_back(
std::string("--mark_as_phish_guard_phishing=") + _phishingURL.spec());
@@ -116,6 +132,19 @@
kWaitForUIElementTimeout];
}
+// Tests that password protection UI is shown even when the webpage cancels
+// keydown events.
+- (void)testPasswordReuseDetectionKeydownPreventDefault {
+ [ChromeEarlGrey loadURL:GURL(_phishingURL.spec() + "?preventDefault=true")];
+ [ChromeEarlGrey waitForWebStateContainingText:kInputPage];
+
+ [self typePasswordIntoWebInput];
+ [ChromeEarlGrey
+ waitForUIElementToAppearWithMatcher:PasswordProtectionMatcher()
+ timeout:base::test::ios::
+ kWaitForUIElementTimeout];
+}
+
// Tests that password protection UI is not shown when saved password is reused
// on safe site.
- (void)testPasswordProtectionNotShownForAllowListedURL {
diff --git a/ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mm b/ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mm
index 41e7ad0..10ad6418 100644
--- a/ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mm
+++ b/ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mm
@@ -8,6 +8,7 @@
#import "base/ios/ios_util.h"
#import "base/no_destructor.h"
#import "base/strings/sys_string_conversions.h"
+#import "base/strings/utf_string_conversions.h"
#import "ios/chrome/browser/safe_browsing/model/input_event_observer.h"
#import "ios/web/public/js_messaging/script_message.h"
@@ -19,7 +20,7 @@
// Values for the "eventType" field in messages received by this feature's
// script message handler.
const char kPasteEventType[] = "TextPasted";
-const char kKeyPressedEventType[] = "KeyPressed";
+const char kKeyDownEventType[] = "KeyDown";
} // namespace
PasswordProtectionJavaScriptFeature::PasswordProtectionJavaScriptFeature()
@@ -71,11 +72,12 @@
return;
}
- if (*event_type == kKeyPressedEventType) {
- // A keypress event should consist of a single character. A longer string
+ if (*event_type == kKeyDownEventType) {
+ // A key event should consist of a single character. A longer string
// means the message isn't well-formed, so might be coming from a
// compromised WebProcess.
- if ((*text).size() > 1) {
+ std::u16string text16 = base::UTF8ToUTF16(*text);
+ if (text16.length() != 1) {
return;
}
observer->OnKeyPressed(*text);
diff --git a/ios/chrome/browser/safe_browsing/model/resources/password_protection.ts b/ios/chrome/browser/safe_browsing/model/resources/password_protection.ts
index 4bd2acdd..80be253 100644
--- a/ios/chrome/browser/safe_browsing/model/resources/password_protection.ts
+++ b/ios/chrome/browser/safe_browsing/model/resources/password_protection.ts
@@ -5,22 +5,23 @@
import {sendWebKitMessage} from '//ios/web/public/js_messaging/resources/utils.js';
/*
-* @fileoverview Adds listeners that forward keypress and paste events to the
-* browser. The browser uses this information to detect and warn the user about
-* situations where the user enters one of their saved passwords on a
-* possibly-unsafe site
-*/
+ * @fileoverview Adds listeners that forward keydown and paste events to the
+ * browser. The browser uses this information to detect and warn the user about
+ * situations where the user enters one of their saved passwords on a
+ * possibly-unsafe site
+ */
/**
- * Listens for keypress events and forwards the entered key to the browser.
+ * Listens for keydown events and forwards the entered key to the browser.
*/
-function onKeypressEvent(event : KeyboardEvent) : void {
- // Only forward events where the entered key has length 1, to avoid forwarding
- // special keys like "Enter".
- if (event.isTrusted && event.key.length === 1) {
+function onKeydownEvent(event: KeyboardEvent): void {
+ // Only forward events where the entered key has length 1, to avoid
+ // forwarding special keys like "Enter".
+ if (event.isTrusted && event.key.length === 1 && !event.ctrlKey &&
+ !event.metaKey) {
sendWebKitMessage(
'PasswordProtectionTextEntered',
- {eventType: 'KeyPressed', text: event.key});
+ {eventType: 'KeyDown', text: event.key});
}
}
@@ -49,5 +50,5 @@
// Events are first dispatched to the window object, in the capture phase of
// JavaScript event dispatch, so listen for them there.
-window.addEventListener('keypress', onKeypressEvent, true);
+window.addEventListener('keydown', onKeydownEvent, true);
window.addEventListener('paste', onPasteEvent, true);
Original Bug Report
Potential PhishGuard bypass on iOS via keydown event cancellation
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 https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The Password Reuse Detection (PhishGuard) feature on iOS relies on a JavaScript listener for keypress events to monitor user input. A malicious webpage can bypass this detection entirely by intercepting and canceling keydown events, which prevents the browser from ever dispatching the keypress event that the security script monitors.
Affected files:
ios/chrome/browser/safe_browsing/model/resources/password_protection.tsios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mm
Estimated timestamp from git blame: 2021-02-11
Background
On iOS, WKWebView lacks native APIs (like RenderWidgetHost::InputEventObserver) to observe user input events directly from the embedder. To support the Password Reuse Detection (PhishGuard) feature, Chrome injects a script (ios/chrome/browser/safe_browsing/model/resources/password_protection.ts) into an isolated world on the page.
This script listens for user keystrokes and forwards them to the native browser process to check against saved passwords. Currently, the script relies exclusively on the keypress and paste events:
// ios/chrome/browser/safe_browsing/model/resources/password_protection.ts
window.addEventListener('keypress', onKeypressEvent, true);
window.addEventListener('paste', onPasteEvent, true);
Vulnerability Details
According to the W3C UI Events specification, the keypress event is dependent on the preceding keydown event. If the default action of a keydown event is prevented (e.g., via event.preventDefault()), the browser engine must not dispatch the subsequent keypress event.
Because the password_protection.ts script only listens for keypress, it is blind to any keystrokes where the preceding keydown event was canceled by the webpage. Since the page’s main-world scripts execute before isolated-world scripts can see derived events, a malicious page can reliably intercept and hide user input.
Potential Exploit Scenario
Note: These are potential steps based on static analysis.
- An attacker hosts a phishing page designed to steal credentials.
- The attacker adds a script to the main world that intercepts
keydownevents on the password input field. - Inside the event listener, the script calls
event.preventDefault(), which stops the browser from generating thekeypressevent. - To simulate normal typing for the victim, the attacker’s script manually reads
event.keyand appends it to the input field’s value. - The victim types their saved password into the field.
- Because
keypressevents are completely suppressed by the browser engine, the isolated worldpassword_protection.tsscript observes nothing. - The native
IOSChromePasswordReuseDetectionManagerClientreceives no keystroke data, the PhishGuard heuristic is never triggered, and the password is stolen without any warning interstitial being shown to the user.
Suggested Fix
The password_protection.ts script should be updated to observe events that cannot be easily suppressed or bypassed by the page’s main world logic.
Potential mitigations include:
- Listen to
keydowninstead ofkeypress: Changing the listener tokeydownin the capture phase ensures the isolated script sees the physical key press before the main world has a chance to callpreventDefault(). - Observe
inputevents: Theinputevent fires whenever the value of an<input>element changes, regardless of whether it was typed naturally, pasted, or modified via a script responding to a canceledkeydownevent. This may provide a more robust way to monitor what text is actually accumulating in the field.
Evaluated with Chrome root at commit: cc901875d53bf4e4fe0e01f02843871da4106e70
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.