CVE-2026-13866
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/android/text_suggestion_host_android.cc |
modified | |
GetJavaTextSuggestionHostcontent/browser/android/text_suggestion_host_android.cc |
modified |
Files Changed
content/browser/android/ime_adapter_android.cccontent/browser/android/text_suggestion_host_android.cc
Patch
From 55052563526e72687f8a19963cd678c017fba6e9 Mon Sep 17 00:00:00 2001
From: Ted Choc <tedchoc@chromium.org>
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 <boliu@chromium.org>
Commit-Queue: Ted Choc <tedchoc@chromium.org>
Reviewed-by: Mustafa Emre Acer <meacer@chromium.org>
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 @@
Original Bug Report
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.cccontent/browser/android/text_suggestion_host_android.hcontent/browser/browser_interface_binders.cccontent/browser/android/text_suggestion_host_mojo_impl_android.ccthird_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:
- Improper Interface Binding:
BindTextSuggestionHostForFrameinbrowser_interface_binders.ccallows the main frame to bind theTextSuggestionHostinterface, but prevents OOPIFs from doing so (becauseRenderWidgetHostViewAndroid::FromRenderWidgetHostViewreturns null for child frames). This means the main frame can always send commands toTextSuggestionHostAndroid. - Confused Deputy in Backend Resolution: When
TextSuggestionHostAndroidneeds to communicate with the renderer (e.g., when a timer expires or a suggestion is selected), it callsGetTextSuggestionBackend(). If the backend is not yet bound, it binds it to the currently focused frame viaWebContentsImpl::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.
- 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.
- The attacker’s compromised main frame binds the
blink::mojom::TextSuggestionHostinterface. - The user taps the misspelled word in the victim OOPIF. This shifts the browser’s focus to the victim OOPIF.
- 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.
- The attacker’s main frame, observing the focus change, calls
TextSuggestionHost::StartSuggestionMenuTimer(). - When the timer expires in the browser,
OnSuggestionMenuTimeout()callsGetTextSuggestionBackend(). Since the victim OOPIF is currently focused, the browser binds itstext_suggestion_backend_remote to the victim and sends aSuggestionMenuTimeoutCallbackto it. - The victim renderer processes this callback, placing an
ActiveSuggestionMarkeron the misspelled word. - The attacker’s main frame then calls
ShowSpellCheckSuggestionMenu()with attacker-controlled strings. - The browser displays the native Android suggestion popup containing the attacker’s text.
- When the user selects the suggestion, the browser calls
ApplySpellCheckSuggestion(). This method uses the cachedtext_suggestion_backend_(bound to the victim OOPIF) to send the replacement text. - 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.