Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Views
DescriptionInappropriate implementation in Views
ComponentViews
Bug ClassLogic Error
Tracker518058990
Fix commitcb92015f0518 (chromium/src) +48/-22
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
ui/base/ime/input_method_base.cc
modified
TEST_F
ui/views/cocoa/bridged_native_widget_unittest.mm
modified

Files Changed

  • ui/base/ime/input_method_base.cc
  • ui/views/cocoa/bridged_native_widget_unittest.mm
  • ui/views/cocoa/text_input_host.mm
From cb92015f0518b7c598904aca5ce37c3093c155e7 Mon Sep 17 00:00:00 2001
From: Avi Drissman <avi@chromium.org>
Date: Mon, 01 Jun 2026 19:37:44 -0700
Subject: [PATCH] Honor TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD on macOS

TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD is set if a text field was a
password field at any point in the past. Other IME code handles it;
update TextInputHost to handle it as well.

Fixed: 518058990
Change-Id: Ib8e65cd97f556a3f469e12e086ee38b46a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7888102
Reviewed-by: Keren Zhu <kerenzhu@chromium.org>
Commit-Queue: Avi Drissman <avi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1639888}
---

diff --git a/ui/base/ime/input_method_base.cc b/ui/base/ime/input_method_base.cc
index e2a557f..99f49321 100644
--- a/ui/base/ime/input_method_base.cc
+++ b/ui/base/ime/input_method_base.cc
@@ -16,6 +16,7 @@
 #include "ui/base/ime/input_method_observer.h"
 #include "ui/base/ime/text_input_client.h"
 #include "ui/base/ime/text_input_flags.h"
+#include "ui/base/ime/text_input_type.h"
 #include "ui/base/ime/virtual_keyboard_controller_stub.h"
 #include "ui/events/event.h"
 
@@ -74,11 +75,15 @@
 
 TextInputType InputMethodBase::GetTextInputType() const {
   TextInputClient* client = GetTextInputClient();
-  return client
-             ? (client->GetTextInputFlags() & TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD
-                    ? TEXT_INPUT_TYPE_PASSWORD
-                    : client->GetTextInputType())
-             : TEXT_INPUT_TYPE_NONE;
+  if (!client) {
+    return TEXT_INPUT_TYPE_NONE;
+  }
+
+  if (client->GetTextInputFlags() & TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD) {
+    return TEXT_INPUT_TYPE_PASSWORD;
+  }
+
+  return client->GetTextInputType();
 }
 
 void InputMethodBase::SetVirtualKeyboardVisibilityIfEnabled(bool should_show) {
diff --git a/ui/views/cocoa/bridged_native_widget_unittest.mm b/ui/views/cocoa/bridged_native_widget_unittest.mm
index fcd6d8d..105a9c38 100644
--- a/ui/views/cocoa/bridged_native_widget_unittest.mm
+++ b/ui/views/cocoa/bridged_native_widget_unittest.mm
@@ -8,6 +8,7 @@
 #include <cstddef>
 #include <memory>
 #include <string>
+#include <string_view>
 
 #import "base/apple/foundation_util.h"
 #import "base/apple/scoped_objc_class_swizzler.h"
@@ -27,6 +28,7 @@
 #include "ui/base/cocoa/find_pasteboard.h"
 #import "ui/base/cocoa/window_size_constants.h"
 #include "ui/base/ime/input_method.h"
+#include "ui/base/ime/text_input_flags.h"
 #include "ui/base/metadata/metadata_header_macros.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
 #import "ui/base/test/cocoa_helper.h"
@@ -567,9 +569,9 @@
   // Install a textfield with input type |text_input_type| in the view hierarchy
   // and make it the text input client. Also initializes |dummy_text_view_|.
   Textfield* InstallTextField(
-      const std::u16string& text,
+      std::u16string_view text,
       ui::TextInputType text_input_type = ui::TEXT_INPUT_TYPE_TEXT);
-  Textfield* InstallTextField(const std::string& text);
+  Textfield* InstallTextField(std::string_view text);
 
   // Returns the actual current text for |ns_view_|, or the selected substring.
   NSString* GetActualText();
@@ -667,7 +669,7 @@
 BridgedNativeWidgetTest::~BridgedNativeWidgetTest() = default;
 
 Textfield* BridgedNativeWidgetTest::InstallTextField(
-    const std::u16string& text,
+    std::u16string_view text,
     ui::TextInputType text_input_type) {
   Textfield* textfield = new Textfield();
   textfield->SetText(text);
@@ -692,7 +694,7 @@
   return textfield;
 }
 
-Textfield* BridgedNativeWidgetTest::InstallTextField(const std::string& text) {
+Textfield* BridgedNativeWidgetTest::InstallTextField(std::string_view text) {
   return InstallTextField(base::ASCIIToUTF16(text));
 }
 
@@ -1087,15 +1089,23 @@
 // Ensure a nil NSTextInputContext is returned when the ui::TextInputClient is
 // not editable, a password field, or unset.
 TEST_F(BridgedNativeWidgetTest, InputContext) {
-  const std::u16string test_string = u"test_str";
+  constexpr std::u16string_view test_string = u"test_str";
+
   InstallTextField(test_string, ui::TEXT_INPUT_TYPE_PASSWORD);
-  EXPECT_FALSE([ns_view_ inputContext]);
-  InstallTextField(test_string, ui::TEXT_INPUT_TYPE_TEXT);
-  EXPECT_TRUE([ns_view_ inputContext]);
+  EXPECT_FALSE(ns_view_.inputContext);
+
+  Textfield* text_field =
+      InstallTextField(test_string, ui::TEXT_INPUT_TYPE_TEXT);
+  EXPECT_TRUE(ns_view_.inputContext);
+
+  text_field->SetTextInputFlags(ui::TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD);
+  EXPECT_FALSE(ns_view_.inputContext);
+
   GetNSWindowHost()->text_input_host()->SetTextInputClient(nullptr);
-  EXPECT_FALSE([ns_view_ inputContext]);
+  EXPECT_FALSE(ns_view_.inputContext);
+
   InstallTextField(test_string, ui::TEXT_INPUT_TYPE_NONE);
-  EXPECT_FALSE([ns_view_ inputContext]);
+  EXPECT_FALSE(ns_view_.inputContext);
 }
 
 // Test getting complete string using text input protocol.
diff --git a/ui/views/cocoa/text_input_host.mm b/ui/views/cocoa/text_input_host.mm
index a252190..2e5aac6 100644
--- a/ui/views/cocoa/text_input_host.mm
+++ b/ui/views/cocoa/text_input_host.mm
@@ -10,6 +10,8 @@
 #include "ui/accelerated_widget_mac/window_resize_helper_mac.h"
 #include "ui/base/cocoa/menu_utils.h"
 #include "ui/base/ime/text_input_client.h"
+#include "ui/base/ime/text_input_flags.h"
+#include "ui/base/ime/text_input_type.h"
 #include "ui/events/keycodes/dom/dom_code.h"
 #include "ui/views/cocoa/native_widget_mac_ns_window_host.h"
 
@@ -238,16 +240,25 @@
   }
 
   // When not in an editable mode, or while entering passwords
-  // (http://crbug.com/23219), we don't want to show IME candidate windows.
+  // (https://crbug.com/41007509), we don't want to show IME candidate windows.
   // Returning nil prevents this view from getting messages defined as part of
   // the NSTextInputClient protocol.
-  switch (pending_text_input_client_->GetTextInputType()) {
-    case ui::TEXT_INPUT_TYPE_NONE:
-    case ui::TEXT_INPUT_TYPE_PASSWORD:
-      return true;
-    default:
-      *out_has_input_context = true;
+  //
+  // Honor TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD: a revealed password/PIN field
+  // reports TEXT_INPUT_TYPE_TEXT but must still be hidden from IMEs. See
+  // InputMethodBase::GetTextInputType().
+  if (pending_text_input_client_->GetTextInputFlags() &
+      ui::TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD) {
+    return true;
   }
+  ui::TextInputType type = pending_text_input_client_->GetTextInputType();
+  if (type == ui::TEXT_INPUT_TYPE_NONE ||
+      type == ui::TEXT_INPUT_TYPE_PASSWORD) {
+    return true;
+  }
+
+  // Otherwise, allow a context.
+  *out_has_input_context = true;
   return true;
 }
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/views/cocoa/bridged_native_widget_unittest.mm b/ui/views/cocoa/bridged_native_widget_unittest.mm
index fcd6d8d..105a9c38 100644
--- a/ui/views/cocoa/bridged_native_widget_unittest.mm
+++ b/ui/views/cocoa/bridged_native_widget_unittest.mm
@@ -8,6 +8,7 @@
 #include <cstddef>
 #include <memory>
 #include <string>
+#include <string_view>
 
 #import "base/apple/foundation_util.h"
 #import "base/apple/scoped_objc_class_swizzler.h"
@@ -27,6 +28,7 @@
 #include "ui/base/cocoa/find_pasteboard.h"
 #import "ui/base/cocoa/window_size_constants.h"
 #include "ui/base/ime/input_method.h"
+#include "ui/base/ime/text_input_flags.h"
 #include "ui/base/metadata/metadata_header_macros.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
 #import "ui/base/test/cocoa_helper.h"
@@ -567,9 +569,9 @@
   // Install a textfield with input type |text_input_type| in the view hierarchy
   // and make it the text input client. Also initializes |dummy_text_view_|.
   Textfield* InstallTextField(
-      const std::u16string& text,
+      std::u16string_view text,
       ui::TextInputType text_input_type = ui::TEXT_INPUT_TYPE_TEXT);
-  Textfield* InstallTextField(const std::string& text);
+  Textfield* InstallTextField(std::string_view text);
 
   // Returns the actual current text for |ns_view_|, or the selected substring.
   NSString* GetActualText();
@@ -667,7 +669,7 @@
 BridgedNativeWidgetTest::~BridgedNativeWidgetTest() = default;
 
 Textfield* BridgedNativeWidgetTest::InstallTextField(
-    const std::u16string& text,
+    std::u16string_view text,
     ui::TextInputType text_input_type) {
   Textfield* textfield = new Textfield();
   textfield->SetText(text);
@@ -692,7 +694,7 @@
   return textfield;
 }
 
-Textfield* BridgedNativeWidgetTest::InstallTextField(const std::string& text) {
+Textfield* BridgedNativeWidgetTest::InstallTextField(std::string_view text) {
   return InstallTextField(base::ASCIIToUTF16(text));
 }
 
@@ -1087,15 +1089,23 @@
 // Ensure a nil NSTextInputContext is returned when the ui::TextInputClient is
 // not editable, a password field, or unset.
 TEST_F(BridgedNativeWidgetTest, InputContext) {
-  const std::u16string test_string = u"test_str";
+  constexpr std::u16string_view test_string = u"test_str";
+
   InstallTextField(test_string, ui::TEXT_INPUT_TYPE_PASSWORD);
-  EXPECT_FALSE([ns_view_ inputContext]);
-  InstallTextField(test_string, ui::TEXT_INPUT_TYPE_TEXT);
-  EXPECT_TRUE([ns_view_ inputContext]);
+  EXPECT_FALSE(ns_view_.inputContext);
+
+  Textfield* text_field =
+      InstallTextField(test_string, ui::TEXT_INPUT_TYPE_TEXT);
+  EXPECT_TRUE(ns_view_.inputContext);
+
+  text_field->SetTextInputFlags(ui::TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD);
+  EXPECT_FALSE(ns_view_.inputContext);
+
   GetNSWindowHost()->text_input_host()->SetTextInputClient(nullptr);
-  EXPECT_FALSE([ns_view_ inputContext]);
+  EXPECT_FALSE(ns_view_.inputContext);
+
   InstallTextField(test_string, ui::TEXT_INPUT_TYPE_NONE);
-  EXPECT_FALSE([ns_view_ inputContext]);
+  EXPECT_FALSE(ns_view_.inputContext);
 }
 
 // Test getting complete string using text input protocol.
Loading diff…

Original Bug Report

reported by vm...@google.com

macOS TextInputHost::HasInputContext ignores TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD

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: On macOS, TextInputHost::HasInputContext queries the raw text input type and ignores the TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD security flag. Consequently, when a password or GPM PIN field is toggled to a visible state, active third-party Input Method Editors (IMEs) can access and potentially log the plaintext credential. This bypasses the platform defense-in-depth protection implemented on Windows, Linux, and ChromeOS.

Affected files:

  • ui/views/cocoa/text_input_host.mm

Estimated timestamp from git blame: 2023-04-20

Root Cause Analysis

The TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD flag was introduced as a defense-in-depth measure to prevent revealed passwords and PINs from being exposed to Input Method Editors (IMEs). On platforms utilizing InputMethodBase, InputMethodBase::GetTextInputType() correctly coerces the returned text type back to TEXT_INPUT_TYPE_PASSWORD when this flag is present to suppress active IMEs.

However, the macOS Views NSTextInputClient bridge does not route through InputMethodBase. Instead, TextInputHost::HasInputContext (in ui/views/cocoa/text_input_host.mm) reads the raw client type directly without querying text input flags:

switch (pending_text_input_client_->GetTextInputType()) {
  case ui::TEXT_INPUT_TYPE_NONE:
  case ui::TEXT_INPUT_TYPE_PASSWORD:
    return true;
  default:
    *out_has_input_context = true;
}

Because the raw client type is returned as TEXT_INPUT_TYPE_TEXT once the credential is eye-toggled/revealed, the IME remains fully active and can query the contents of the textfield. Furthermore, Textfield::ImeEditingAllowed() in ui/views/controls/textfield/textfield.cc only checks the raw type, allowing the IME to query and learn the plaintext credential.

Affected Components

This potential security gap renders the toggle-reveal protection ineffective on macOS in the following views:

  1. Save/Update Password Bubble: EditablePasswordCombobox (via PasswordSaveUpdateView in chrome/browser/ui/views/passwords/password_save_update_view.cc).
  2. WebAuthn GPM PIN Sheet: AuthenticatorGPMArbitraryPinView (in chrome/browser/ui/views/webauthn/authenticator_gpm_arbitrary_pin_view.cc).

Potential Trigger Path

Note: These are potential steps based on static analysis; our tooling does not currently support code execution or verification on live systems.

  1. A macOS user with an active third-party IME opens the Save Password bubble or the WebAuthn GPM PIN view.
  2. The user clicks the eye icon to reveal the password or PIN. This toggles the textfield type to TEXT_INPUT_TYPE_TEXT and appends TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD to the input flags.
  3. The user focuses the revealed textfield. AppKit queries -[BridgedContentView inputContext], which calls TextInputHost::HasInputContext.
  4. HasInputContext retrieves TEXT_INPUT_TYPE_TEXT as the raw type, and sets out_has_input_context = true.
  5. Because a non-nil NSTextInputContext is returned, the active third-party IME queries text via standard NSTextInputClient protocols (e.g., -attributedSubstringForProposedRange:actualRange:).
  6. Since Textfield::ImeEditingAllowed() only checks the raw type, it allows the IME to query and read the plaintext credential.

Suggested Fix

  1. Update TextInputHost::HasInputContext in ui/views/cocoa/text_input_host.mm to check the client’s text input flags:
ui::TextInputType type = pending_text_input_client_->GetTextInputType();
if (pending_text_input_client_->GetTextInputFlags() &
    ui::TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD) {
  type = ui::TEXT_INPUT_TYPE_PASSWORD;
}

switch (type) {
  case ui::TEXT_INPUT_TYPE_NONE:
  case ui::TEXT_INPUT_TYPE_PASSWORD:
    return true;
  default:
    *out_has_input_context = true;
}
  1. Update Textfield::ImeEditingAllowed in ui/views/controls/textfield/textfield.cc to check for this flag as well:
bool Textfield::ImeEditingAllowed() const {
  if (GetTextInputFlags() & ui::TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD) {
    return false;
  }
  ui::TextInputType t = GetTextInputType();
  return (t != ui::TEXT_INPUT_TYPE_NONE && t != ui::TEXT_INPUT_TYPE_PASSWORD);
}

Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040


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.

View on issue tracker