CVE-2026-17874
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fios/chrome/browser/safe_browsing/model/password_protection_java_script_feature_unittest.mm |
modified |
Files Changed
ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mmios/chrome/browser/safe_browsing/model/password_protection_java_script_feature_unittest.mmios/chrome/browser/safe_browsing/model/resources/password_protection.ts
Patch
From 4ff1a8c33cceab364ec8c771495e1873e7e99cdb Mon Sep 17 00:00:00 2001 From: Alexis Hétu <[email protected]> Date: Wed, 10 Jun 2026 12:06:22 -0700 Subject: [PATCH] [iOS][Safe Browsing] Verify Unicode code points in password protection Update keydown event validation to check the number of Unicode scalar values (code points) rather than UTF-16 code units. This ensures single key presses involving supplementary characters (such as non-BMP symbols) are correctly validated and handled. Bug: 522074154 Change-Id: I16a8974e9a09eb1759a9371b62e4d2e0cf2e272b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7920347 Commit-Queue: Alexis Hétu <[email protected]> Reviewed-by: Joemer Ramos <[email protected]> Cr-Commit-Position: refs/heads/main@{#1644782} --- 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 10981fd..f3ee7df 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,7 +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 "base/strings/utf_string_conversion_utils.h" #import "ios/chrome/browser/safe_browsing/model/input_event_observer.h" #import "ios/web/public/js_messaging/script_message.h" @@ -78,8 +78,7 @@ // 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. - std::u16string text16 = base::UTF8ToUTF16(*text); - if (text16.length() != 1) { + if (base::CountUnicodeCharacters(*text) != 1) { return; } observer->OnKeyPressed(*text); diff --git a/ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature_unittest.mm b/ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature_unittest.mm index 5c08e70..c322ad3 100644 --- a/ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature_unittest.mm +++ b/ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature_unittest.mm @@ -112,4 +112,43 @@ EXPECT_TRUE(observer_->on_paste_called_); } +// Tests that KeyDown events are correctly checked for a single Unicode code +// point. +TEST_F(PasswordProtectionJavaScriptFeatureTest, KeyDownEventLengthCheck) { + // A single ASCII character (1 code point, 1 code unit in UTF-16). + base::Value body1( + base::DictValue().Set("eventType", "KeyDown").Set("text", "a")); + web::ScriptMessage message1(std::make_unique<base::Value>(std::move(body1)), + /*is_user_interacting=*/true, + /*is_main_frame=*/true, + /*request_url=*/std::nullopt, url::Origin()); + feature_->ScriptMessageReceived(&web_state_, message1); + EXPECT_TRUE(observer_->on_key_pressed_called_); + observer_->on_key_pressed_called_ = false; + + // A single supplementary Unicode code point (e.g., U+1F600 Grinning Face + // emoji). It takes 2 UTF-16 code units (surrogate pair) but is 1 Unicode code + // point. + base::Value body2(base::DictValue() + .Set("eventType", "KeyDown") + .Set("text", "\xF0\x9F\x98\x80")); + web::ScriptMessage message2(std::make_unique<base::Value>(std::move(body2)), + /*is_user_interacting=*/true, + /*is_main_frame=*/true, + /*request_url=*/std::nullopt, url::Origin()); + feature_->ScriptMessageReceived(&web_state_, message2); + EXPECT_TRUE(observer_->on_key_pressed_called_); + observer_->on_key_pressed_called_ = false; + + // Multiple characters should be dropped. + base::Value body3( + base::DictValue().Set("eventType", "KeyDown").Set("text", "ab")); + web::ScriptMessage message3(std::make_unique<base::Value>(std::move(body3)), + /*is_user_interacting=*/true, + /*is_main_frame=*/true, + /*request_url=*/std::nullopt, url::Origin()); + feature_->ScriptMessageReceived(&web_state_, message3); + EXPECT_FALSE(observer_->on_key_pressed_called_); +} + } // namespace 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 80be253..eaeb1b4 100644 --- a/ios/chrome/browser/safe_browsing/model/resources/password_protection.ts +++ b/ios/chrome/browser/safe_browsing/model/resources/password_protection.ts @@ -17,7 +17,7 @@ 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 && + if (event.isTrusted && [...event.key].length === 1 && !event.ctrlKey && !event.metaKey) { sendWebKitMessage( 'PasswordProtectionTextEntered',
Regression Test / PoC
diff --git a/ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature_unittest.mm b/ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature_unittest.mm
index 5c08e70..c322ad3 100644
--- a/ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature_unittest.mm
+++ b/ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature_unittest.mm
@@ -112,4 +112,43 @@
EXPECT_TRUE(observer_->on_paste_called_);
}
+// Tests that KeyDown events are correctly checked for a single Unicode code
+// point.
+TEST_F(PasswordProtectionJavaScriptFeatureTest, KeyDownEventLengthCheck) {
+ // A single ASCII character (1 code point, 1 code unit in UTF-16).
+ base::Value body1(
+ base::DictValue().Set("eventType", "KeyDown").Set("text", "a"));
+ web::ScriptMessage message1(std::make_unique<base::Value>(std::move(body1)),
+ /*is_user_interacting=*/true,
+ /*is_main_frame=*/true,
+ /*request_url=*/std::nullopt, url::Origin());
+ feature_->ScriptMessageReceived(&web_state_, message1);
+ EXPECT_TRUE(observer_->on_key_pressed_called_);
+ observer_->on_key_pressed_called_ = false;
+
+ // A single supplementary Unicode code point (e.g., U+1F600 Grinning Face
+ // emoji). It takes 2 UTF-16 code units (surrogate pair) but is 1 Unicode code
+ // point.
+ base::Value body2(base::DictValue()
+ .Set("eventType", "KeyDown")
+ .Set("text", "\xF0\x9F\x98\x80"));
+ web::ScriptMessage message2(std::make_unique<base::Value>(std::move(body2)),
+ /*is_user_interacting=*/true,
+ /*is_main_frame=*/true,
+ /*request_url=*/std::nullopt, url::Origin());
+ feature_->ScriptMessageReceived(&web_state_, message2);
+ EXPECT_TRUE(observer_->on_key_pressed_called_);
+ observer_->on_key_pressed_called_ = false;
+
+ // Multiple characters should be dropped.
+ base::Value body3(
+ base::DictValue().Set("eventType", "KeyDown").Set("text", "ab"));
+ web::ScriptMessage message3(std::make_unique<base::Value>(std::move(body3)),
+ /*is_user_interacting=*/true,
+ /*is_main_frame=*/true,
+ /*request_url=*/std::nullopt, url::Origin());
+ feature_->ScriptMessageReceived(&web_state_, message3);
+ EXPECT_FALSE(observer_->on_key_pressed_called_);
+}
+
} // namespace
Original Bug Report
Potential iOS PhishGuard Bypass via Non-BMP Characters (Emojis) in Passwords
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The Safe Browsing password reuse detection (PhishGuard) on iOS incorrectly drops keystrokes for characters outside the Basic Multilingual Plane (BMP), such as emojis. This is because both the injected JavaScript and the native C++ receiver mandate a UTF-16 string length of exactly 1, which excludes surrogate pairs. Consequently, passwords containing these characters can be entered into phishing sites without triggering a warning.
Affected files:
ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mmios/chrome/browser/safe_browsing/model/resources/password_protection.ts
Estimated timestamp from git blame: 2021-02-11
The PhishGuard implementation on iOS, which provides Safe Browsing password reuse detection, contains a logic error in how it handles keystrokes for non-BMP characters. The system monitors user input by listening for keydown events in an injected JavaScript file (ios/chrome/browser/safe_browsing/model/resources/password_protection.ts) and forwarding the event.key value to the browser process.
Both the JavaScript and C++ components enforce a restriction that the captured key’s text length must be exactly 1 UTF-16 code unit. This check is intended to exclude special keys like ‘Enter’ or ‘Shift’. However, because JavaScript strings and C++ std::u16string represent characters outside the Basic Multilingual Plane (BMP) as surrogate pairs (length 2), this logic inadvertently excludes them. This includes emojis and certain mathematical symbols.
In password_protection.ts:
if (event.isTrusted && event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
sendWebKitMessage('PasswordProtectionTextEntered', {eventType: 'KeyDown', text: event.key});
}
In ios/chrome/browser/safe_browsing/model/password_protection_java_script_feature.mm:
std::u16string text16 = base::UTF8ToUTF16(*text);
if (text16.length() != 1) {
return;
}
Because these non-BMP characters are silently dropped, the PasswordReuseDetectionManager fails to receive the full password sequence when it is typed into a phishing site. For example, if a user’s saved password is mypassword😀, only mypassword will be recorded in the internal keystroke buffer. The subsequent hash comparison against the user’s stored password hashes will fail. Consequently, the user will not receive a PhishGuard warning, potentially allowing an attacker to capture their password.
Potential Steps to Reproduce
Note: These steps represent a potential attack sequence, as our tooling has not executed this against a live target.
- An attacker sets up a malicious webpage that visually mimics a legitimate login portal.
- A victim, who has a saved password for the legitimate site containing at least one non-BMP character (e.g.,
mypassword😀), navigates to the phishing site using Chrome on iOS. - The victim types their saved password into the site’s input field.
- As they type, the injected
password_protection.tsscript successfully captures and forwards the BMP characters (m,y,p, etc.). - When the victim types the non-BMP character (
😀),event.key.lengthevaluates to2. The script silently drops the keystroke. - The PhishGuard buffer in the browser process only records
mypassword. - The hash comparison fails, bypassing the Safe Browsing warning interstitial entirely.
Suggested Fix
The length check should be updated to verify the number of Unicode scalar values (code points) rather than UTF-16 code units.
In password_protection.ts, this can be achieved by checking if the string contains a single code point (e.g., using Array.from(event.key).length === 1 or [...event.key].length === 1).
In password_protection_java_script_feature.mm, the C++ check should ensure the string corresponds to a single Unicode code point rather than text16.length() == 1.
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
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.