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
Tracker504185807
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]

iOS PhishGuard bypass when typing passwords containing non-ASCII characters

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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A logic error in iOS Chrome’s password reuse detection causes non-ASCII keystrokes to be silently dropped. This prevents PhishGuard from warning users if they type a saved password containing non-ASCII characters into a phishing site. The issue is caused by comparing the byte-size of a UTF-8 string against a length check designed for single characters.

Affected files:

  • ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mm
  • ios/chrome/browser/safe_browsing/model/resources/password_protection.ts

Estimated timestamp from git blame: 2021-02-27

Summary

A logic error in the iOS implementation of PhishGuard (Safe Browsing’s password reuse detection) causes all non-ASCII characters to be silently dropped before they reach the detection manager. As a result, PhishGuard warnings are never triggered if a user types a password containing one or more non-ASCII characters (such as ‘é’, ‘ñ’, or ‘ß’) into a phishing site.

Technical Details

On iOS, password reuse detection utilizes a JavaScript feature to forward keystrokes from the web page to the browser process. In ios/chrome/browser/safe_browsing/model/resources/password_protection.ts, the onKeypressEvent function forwards keystrokes where event.key.length === 1:

function onKeypressEvent(event : KeyboardEvent) : void {
  if (event.isTrusted && event.key.length === 1) {
    sendWebKitMessage(
        'PasswordProtectionTextEntered',
        {eventType: 'KeyPressed', text: event.key});
  }
}

In JavaScript, String.length counts UTF-16 code units. Most non-ASCII characters in the Basic Multilingual Plane (BMP), like ‘é’, have a length of 1, so the script forwards them correctly.

When this message is received in the browser process by PasswordProtectionJavaScriptFeature::ScriptMessageReceived (in ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mm), the text is retrieved as a UTF-8 encoded std::string. The code then performs the following validation:

  if (*event_type == kKeyPressedEventType) {
    // A keypress 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) {
      return;
    }
    observer->OnKeyPressed(*text);
  }

While event.key.length === 1 in JavaScript for a character like ‘é’, its UTF-8 representation in C++ is 2 bytes long (0xC3 0xA9). Because of the (*text).size() > 1 check, the character is rejected and dropped.

Consequently, the PasswordReuseDetectionManager’s running buffer of typed characters never receives the complete password. When the user finishes typing, the internal hash comparison fails because the recorded string (e.g., “caf1234”) does not match the saved password (“café1234”). This bypasses the PhishGuard warning entirely. This issue is specific to iOS; Desktop and Android use a different input event observation mechanism.

Potential Reproduction Steps

Note: These are suggested steps based on source code analysis.

  1. On iOS Chrome, save a password containing at least one non-ASCII BMP character (e.g., ‘café1234’) for a test site.
  2. Ensure Safe Browsing / Password Protection is enabled in Chrome settings.
  3. Navigate to an unrelated, non-allowlisted site (or a known phishing test site).
  4. Type the saved password into any text or password field using a keyboard layout that emits keypress events for the accented character.
  5. Observe that the non-ASCII character (e.g., ‘é’) is dropped in the browser process due to the size check, and no PhishGuard warning is displayed.
  6. Compare this with an all-ASCII password of the same length, which triggers the warning as expected.

Suggested Fix

The size check in PasswordProtectionJavaScriptFeature::ScriptMessageReceived needs to be updated to handle UTF-8 string lengths properly. Instead of checking the byte size ((*text).size() > 1), the code should convert the string to std::u16string or std::u32string and check its length, or verify that the UTF-8 string represents a single valid Unicode character. For example, using base::UTF8ToUTF16(*text).size() > 1 would correctly reject long strings while allowing single non-ASCII characters.

Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646


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