Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Input
DescriptionInsufficient validation of untrusted input in Input
ComponentInput
Bug ClassLogic Error
Tracker497207698
Fix commit55052563526e (chromium/src) +519/-410
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
content/browser/android/text_suggestion_host_android.cc
modified
GetJavaTextSuggestionHost
content/browser/android/text_suggestion_host_android.cc
modified

Files Changed

  • content/browser/android/ime_adapter_android.cc
  • content/browser/android/text_suggestion_host_android.cc
From 55052563526e72687f8a19963cd678c017fba6e9 Mon Sep 17 00:00:00 2001
From: Ted Choc <[email protected]>
Date: Fri, 15 May 2026 12:57:09 -0700
Subject: [PATCH] Update TextSuggestionHost to be owned by the render frame.

Avoid issues with frame management for text suggestions.

Bug: 497207698
Change-Id: If69506837b4b2c03e737ac1d866e38790806ee4d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7750269
Reviewed-by: Bo Liu <[email protected]>
Commit-Queue: Ted Choc <[email protected]>
Reviewed-by: Mustafa Emre Acer <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1631483}
---

diff --git a/content/browser/android/ime_adapter_android.cc b/content/browser/android/ime_adapter_android.cc
index c890135..aed4909 100644
--- a/content/browser/android/ime_adapter_android.cc
+++ b/content/browser/android/ime_adapter_android.cc
@@ -178,9 +178,6 @@
 
 ImeAdapterAndroid::ImeAdapterAndroid(WebContents* web_contents)
     : RenderWidgetHostConnector(web_contents), rwhva_(nullptr) {
-  // Set up mojo client for TextSuggestionHost in advance. Java side is
-  // initialized lazily right before showing the menu first time.
-  TextSuggestionHostAndroid::Create(AttachCurrentThread(), web_contents);
 }
 
 ImeAdapterAndroid::~ImeAdapterAndroid() {
diff --git a/content/browser/android/text_suggestion_host_android.cc b/content/browser/android/text_suggestion_host_android.cc
index 8af21b63..c477597f 100644
--- a/content/browser/android/text_suggestion_host_android.cc
+++ b/content/browser/android/text_suggestion_host_android.cc
@@ -10,16 +10,17 @@
 #include "base/functional/bind.h"
 #include "content/browser/android/text_suggestion_host_mojo_impl_android.h"
 #include "content/browser/renderer_host/render_widget_host_impl.h"
-#include "content/browser/web_contents/web_contents_impl.h"
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
 #include "services/service_manager/public/cpp/interface_provider.h"
 #include "ui/gfx/android/view_configuration.h"
 
 // Must come after all headers that specialize FromJniType() / ToJniType().
 #include "content/public/android/content_jni_headers/SuggestionInfo_jni.h"
 #include "content/public/android/content_jni_headers/TextSuggestionHost_jni.h"
+#include "content/public/android/content_jni_headers/TextSuggestionPopupController_jni.h"
 
 using base::android::AttachCurrentThread;
 using base::android::ConvertJavaStringToUTF8;
@@ -36,15 +37,10 @@
 
 }  // namespace
 
-void TextSuggestionHostAndroid::Create(JNIEnv* env, WebContents* web_contents) {
-  auto* text_suggestion_host = new TextSuggestionHostAndroid(env, web_contents);
-  text_suggestion_host->Initialize();
-}
+DOCUMENT_USER_DATA_KEY_IMPL(TextSuggestionHostAndroid);
 
-TextSuggestionHostAndroid::TextSuggestionHostAndroid(JNIEnv* env,
-                                                     WebContents* web_contents)
-    : RenderWidgetHostConnector(web_contents),
-      rwhva_(nullptr),
+TextSuggestionHostAndroid::TextSuggestionHostAndroid(RenderFrameHost* rfh)
+    : DocumentUserData<TextSuggestionHostAndroid>(rfh),
       suggestion_menu_timeout_(
           base::BindRepeating(
               &TextSuggestionHostAndroid::OnSuggestionMenuTimeout,
@@ -52,21 +48,14 @@
           GetUIThreadTaskRunner({BrowserTaskType::kUserInput})) {}
 
 TextSuggestionHostAndroid::~TextSuggestionHostAndroid() {
-  JNIEnv* env = AttachCurrentThread();
-  ScopedJavaLocalRef<jobject> obj = java_text_suggestion_host_.get(env);
-  if (!obj.is_null())
-    Java_TextSuggestionHost_onNativeDestroyed(env, obj);
-}
-
-void TextSuggestionHostAndroid::UpdateRenderProcessConnection(
-    RenderWidgetHostViewAndroid* old_rwhva,
-    RenderWidgetHostViewAndroid* new_rwhva) {
-  text_suggestion_backend_.reset();
-  if (old_rwhva)
-    old_rwhva->set_text_suggestion_host(nullptr);
-  if (new_rwhva)
-    new_rwhva->set_text_suggestion_host(this);
-  rwhva_ = new_rwhva;
+  WebContents* web_contents =
+      WebContents::FromRenderFrameHost(&render_frame_host());
+  if (web_contents) {
+    JNIEnv* env = base::android::AttachCurrentThread();
+    Java_TextSuggestionPopupController_onNativeTextSuggestionHostDestroyed(
+        env, web_contents->GetJavaWebContents(),
+        reinterpret_cast<intptr_t>(this));
+  }
 }
 
 void TextSuggestionHostAndroid::ApplySpellCheckSuggestion(
@@ -118,19 +107,6 @@
   text_suggestion_backend->OnSuggestionMenuClosed();
 }
 
-ScopedJavaLocalRef<jobject>
-TextSuggestionHostAndroid::GetJavaTextSuggestionHost() {
-  JNIEnv* env = AttachCurrentThread();
-  ScopedJavaLocalRef<jobject> obj = java_text_suggestion_host_.get(env);
-  if (obj.is_null()) {
-    obj = Java_TextSuggestionHost_create(
-        env, RenderWidgetHostConnector::web_contents()->GetJavaWebContents(),
-        reinterpret_cast<intptr_t>(this));
-    java_text_suggestion_host_ = JavaObjectWeakGlobalRef(env, obj);
-  }
-  return obj;
-}
-
 void TextSuggestionHostAndroid::ShowSpellCheckSuggestionMenu(
     double caret_x,
     double caret_y,
@@ -141,13 +117,17 @@
   // tries to send bad input.
   for (size_t i = 0; i < suggestions.size() && i < kMaxNumberOfSuggestions; ++i)
     suggestion_strings.push_back(suggestions[i]->suggestion);
-  JNIEnv* env = AttachCurrentThread();
-  ScopedJavaLocalRef<jobject> obj = GetJavaTextSuggestionHost();
-  if (obj.is_null())
-    return;
 
-  Java_TextSuggestionHost_showSpellCheckSuggestionMenu(
-      env, obj, caret_x, caret_y, ConvertUTF8ToJavaString(env, marked_text),
+  WebContents* web_contents =
+      WebContents::FromRenderFrameHost(&render_frame_host());
+  if (!web_contents) {
+    return;
+  }
+
+  JNIEnv* env = AttachCurrentThread();
+  Java_TextSuggestionPopupController_showSpellCheckSuggestionMenu(
+      env, web_contents->GetJavaWebContents(), reinterpret_cast<intptr_t>(this),
+      caret_x, caret_y, ConvertUTF8ToJavaString(env, marked_text),
       ToJavaArrayOfStrings(env, suggestion_strings));
 }
 
@@ -156,10 +136,13 @@
     double caret_y,
     const std::string& marked_text,
     const std::vector<blink::mojom::TextSuggestionPtr>& suggestions) {
-  JNIEnv* env = AttachCurrentThread();
-  ScopedJavaLocalRef<jobject> obj = GetJavaTextSuggestionHost();
-  if (obj.is_null())
+  WebContents* web_contents =
+      WebContents::FromRenderFrameHost(&render_frame_host());
+  if (!web_contents) {
     return;
+  }
+
+  JNIEnv* env = AttachCurrentThread();
 
   // Enforce kMaxNumberOfSuggestions here in case the renderer is hijacked and
   // tries to send bad input.
@@ -178,8 +161,9 @@
         ConvertUTF8ToJavaString(env, suggestion_ptr->suffix));
   }
 
-  Java_TextSuggestionHost_showTextSuggestionMenu(
-      env, obj, caret_x, caret_y, ConvertUTF8ToJavaString(env, marked_text),
+  Java_TextSuggestionPopupController_showTextSuggestionMenu(
+      env, web_contents->GetJavaWebContents(), reinterpret_cast<intptr_t>(this),
+      caret_x, caret_y, ConvertUTF8ToJavaString(env, marked_text),
       jsuggestion_infos);
 }
 
@@ -189,15 +173,16 @@
       base::Milliseconds(gfx::ViewConfiguration::GetDoubleTapTimeoutInMs()));
 }
 
-void TextSuggestionHostAndroid::OnKeyEvent() {
-  suggestion_menu_timeout_.Stop();
-
-  JNIEnv* env = AttachCurrentThread();
-  ScopedJavaLocalRef<jobject> obj = java_text_suggestion_host_.get(env);
-  if (obj.is_null())
+void TextSuggestionHostAndroid::HidePopups() {
+  WebContents* web_contents =
+      WebContents::FromRenderFrameHost(&render_frame_host());
+  if (!web_contents) {
     return;
+  }
 
-  Java_TextSuggestionHost_hidePopups(env, obj);
+  JNIEnv* env = base::android::AttachCurrentThread();
+  Java_TextSuggestionPopupController_hidePopups(
+      env, web_contents->GetJavaWebContents());
 }
 
 void TextSuggestionHostAndroid::StopSuggestionMenuTimer() {
@@ -210,28 +195,11 @@
Loading diff…

Original Bug Report

reported by [email protected]

Potential cross-site text injection via TextSuggestionHostAndroid confused deputy

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised main frame renderer can inject arbitrary text into a cross-origin OOPIF by exploiting a confused deputy flaw in TextSuggestionHostAndroid. The browser fails to isolate suggestion menu commands to the frame that requested them, allowing an attacker to spoof suggestions for a focused victim frame.

Affected files:

  • content/browser/android/text_suggestion_host_android.cc
  • content/browser/android/text_suggestion_host_android.h
  • content/browser/browser_interface_binders.cc
  • content/browser/android/text_suggestion_host_mojo_impl_android.cc
  • third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.cc

Estimated timestamp from git blame: 2023-11-08

Summary

A vulnerability in TextSuggestionHostAndroid allows a compromised main frame renderer to perform cross-origin text injection into a victim out-of-process iframe (OOPIF). The flaw exists because the browser-side host caches a TextSuggestionBackend remote that points to the currently focused frame, but allows the main frame to trigger the suggestion menu and supply its contents. This acts as a confused deputy, resulting in a Site Isolation bypass where attacker-controlled text is injected into a cross-origin frame.

Technical Details

TextSuggestionHostAndroid is a per-WebContents object on Android that manages the native text suggestion menu. It maintains a mojo::Remote<blink::mojom::TextSuggestionBackend> member (text_suggestion_backend_) used to communicate with the renderer.

There are two primary issues that lead to this vulnerability:

  1. Improper Interface Binding: BindTextSuggestionHostForFrame in browser_interface_binders.cc allows the main frame to bind the TextSuggestionHost interface, but prevents OOPIFs from doing so (because RenderWidgetHostViewAndroid::FromRenderWidgetHostView returns null for child frames). This means the main frame can always send commands to TextSuggestionHostAndroid.
  2. Confused Deputy in Backend Resolution: When TextSuggestionHostAndroid needs to communicate with the renderer (e.g., when a timer expires or a suggestion is selected), it calls GetTextSuggestionBackend(). If the backend is not yet bound, it binds it to the currently focused frame via WebContentsImpl::GetFocusedFrame().

Because the attacker’s main frame can invoke methods like StartSuggestionMenuTimer and ShowSpellCheckSuggestionMenu, it can force the browser to bind the backend to a focused victim OOPIF and then supply malicious text that the browser will route to that OOPIF.

Potential Attack Scenario

Note: These are suggested steps based on code analysis; our tooling agent does not yet have the ability to run code to verify this with a live exploit.

  1. A user visits a malicious site (the attacker) that embeds a victim site in an OOPIF. The victim site has an editable field with a misspelled word.
  2. The attacker’s compromised main frame binds the blink::mojom::TextSuggestionHost interface.
  3. The user taps the misspelled word in the victim OOPIF. This shifts the browser’s focus to the victim OOPIF.
  4. The victim OOPIF attempts to bind the suggestion host to show a menu, but the browser drops the bind request because it is an OOPIF.
  5. The attacker’s main frame, observing the focus change, calls TextSuggestionHost::StartSuggestionMenuTimer().
  6. When the timer expires in the browser, OnSuggestionMenuTimeout() calls GetTextSuggestionBackend(). Since the victim OOPIF is currently focused, the browser binds its text_suggestion_backend_ remote to the victim and sends a SuggestionMenuTimeoutCallback to it.
  7. The victim renderer processes this callback, placing an ActiveSuggestionMarker on the misspelled word.
  8. The attacker’s main frame then calls ShowSpellCheckSuggestionMenu() with attacker-controlled strings.
  9. The browser displays the native Android suggestion popup containing the attacker’s text.
  10. When the user selects the suggestion, the browser calls ApplySpellCheckSuggestion(). This method uses the cached text_suggestion_backend_ (bound to the victim OOPIF) to send the replacement text.
  11. The victim frame receives the command and replaces the misspelled word with the attacker’s payload, successfully bypassing Site Isolation.

Suggested Fix

TextSuggestionHost should be refactored to be a true per-frame interface rather than a per-WebContents/per-RenderWidgetHostViewAndroid object.

Alternatively, TextSuggestionHostMojoImplAndroid should store the RenderFrameHost that bound it, and methods like ShowSpellCheckSuggestionMenu and StartSuggestionMenuTimer should verify that the requesting frame matches the currently focused frame before proceeding. The text_suggestion_backend_ should also be strictly tied to the frame that legitimately triggered the suggestion menu.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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