Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in Safe Browsing
DescriptionIncorrect security UI in Safe Browsing
ComponentSafe Browsing
Bug ClassLogic Error
Tracker508259433
Fix commitff8491ed87d4 (chromium/src) +50/-18
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mm
modified
if
ios/chrome/browser/safe_browsing/model/resources/password_protection.ts
modified

Files Changed

  • ios/chrome/browser/safe_browsing/model/password_protection_egtest.mm
  • ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mm
  • ios/chrome/browser/safe_browsing/model/resources/password_protection.ts
From ff8491ed87d451863a19afbb7d78d490d6f957e2 Mon Sep 17 00:00:00 2001
From: Joshua Hood <[email protected]>
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 <[email protected]>
Commit-Queue: jdh <[email protected]>
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);
Loading diff…

Original Bug Report

reported by [email protected]

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

  1. An attacker hosts a phishing page designed to steal credentials.
  2. The attacker adds a script to the main world that intercepts keydown events on the password input field.
  3. Inside the event listener, the script calls event.preventDefault(), which stops the browser from generating the keypress event.
  4. To simulate normal typing for the victim, the attacker’s script manually reads event.key and appends it to the input field’s value.
  5. The victim types their saved password into the field.
  6. Because keypress events are completely suppressed by the browser engine, the isolated world password_protection.ts script observes nothing.
  7. The native IOSChromePasswordReuseDetectionManagerClient receives 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:

  1. Listen to keydown instead of keypress: Changing the listener to keydown in the capture phase ensures the isolated script sees the physical key press before the main world has a chance to call preventDefault().
  2. Observe input events: The input event 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 canceled keydown event. 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.

View on issue tracker