CVE-2026-113018
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/render_widget_host_view_aura.cc |
modified | |
ifcontent/browser/renderer_host/render_widget_host_view_mac.mm |
modified | |
ifcontent/browser/renderer_host/text_input_manager.cc |
modified | |
TEST_Fcontent/browser/renderer_host/render_widget_host_view_aura_unittest.cc |
modified | |
EXPECT_TRUEcontent/browser/renderer_host/render_widget_host_view_aura_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/render_widget_host_view_mac.mmcontent/browser/renderer_host/render_widget_host_view_aura.cccontent/browser/renderer_host/text_input_manager.cccontent/browser/renderer_host/text_input_manager.hcontent/browser/renderer_host/render_widget_host_view_aura_unittest.cccontent/browser/renderer_host/render_widget_host_view_aura.hcontent/browser/renderer_host/render_widget_host_view_mac_unittest.mmcontent/browser/renderer_host/render_widget_host_impl.hcontent/browser/renderer_host/text_input_manager_unittest.ccchrome/browser/site_isolation/site_per_process_text_input_interactive_uitest.cc
Patch
From cfa51540e478dc1096448e4eeb3e2c1012a70977 Mon Sep 17 00:00:00 2001 From: Jonathan Ross <[email protected]> Date: Tue, 22 Sep 2026 07:14:05 -0700 Subject: [PATCH] Validate view focus before updating TextInputManager active view TextInputManager coordinates text input state across RenderWidgetHostViewBase instances. Previously, UpdateTextInputState did not verify whether the reporting view possessed frame focus before assigning it as the active view. As a result, an unfocused frame sending a text input state update could inadvertently override the active view and clear state for the currently focused frame. This change adds browser-side focus verification before updating the active view in TextInputManager. It also ensures that RenderWidgetHostViewAura only dispatches selection bounds notifications and composition monitoring requests for the currently active view. We also update popup focus and virtual keyboards to validate their focus. TAG=agy Bug: 497072295 Change-Id: I5850a657f7963b911ce31514c567763e267bd6f7 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8420104 Reviewed-by: Mustaq Ahmed <[email protected]> Commit-Queue: Jonathan Ross <[email protected]> Reviewed-by: Łukasz Anforowicz <[email protected]> Cr-Commit-Position: refs/heads/main@{#1702586} --- diff --git a/content/browser/renderer_host/render_widget_host_impl.h b/content/browser/renderer_host/render_widget_host_impl.h index 3c8338e..79f2715 100644 --- a/content/browser/renderer_host/render_widget_host_impl.h +++ b/content/browser/renderer_host/render_widget_host_impl.h @@ -246,6 +246,10 @@ const viz::FrameSinkId& GetFrameSinkId() override; std::optional<GlobalRenderFrameHostId> GetPopupCreatorFrameId() const override; + void set_popup_creator_frame_id_for_testing( + std::optional<GlobalRenderFrameHostId> id) { + popup_creator_frame_id_ = id; + } void UpdateTextDirection(base::i18n::TextDirection direction) override; void NotifyTextDirection() override; void SetExternallySourcedComposition( diff --git a/content/browser/renderer_host/render_widget_host_view_aura.cc b/content/browser/renderer_host/render_widget_host_view_aura.cc index 709b0f02..a8fd1b7 100644 --- a/content/browser/renderer_host/render_widget_host_view_aura.cc +++ b/content/browser/renderer_host/render_widget_host_view_aura.cc @@ -3655,7 +3655,14 @@ return true; }; - if (did_update_state) { + const bool is_active_view = + text_input_manager->GetActiveWidget() && + text_input_manager->GetActiveWidget()->GetView() == updated_view; + + const ui::TextInputType current_type = GetTextInputType(); + if (did_update_state && + (is_active_view || current_type != last_text_input_type_)) { + last_text_input_type_ = current_type; GetInputMethod()->OnTextInputTypeChanged(this); if (!check_alive()) { return; @@ -3666,7 +3673,7 @@ text_input_manager_->GetTextInputState(); #if BUILDFLAG(IS_CHROMEOS) - if (state && state->type != ui::TEXT_INPUT_TYPE_NONE) { + if (is_active_view && state && state->type != ui::TEXT_INPUT_TYPE_NONE) { if (state->last_vk_visibility_request == ui::mojom::VirtualKeyboardVisibilityRequest::SHOW) { GetInputMethod()->SetVirtualKeyboardVisibilityIfEnabled(true); @@ -3684,7 +3691,7 @@ #endif // Show the virtual keyboard if needed. - if (state && state->type != ui::TEXT_INPUT_TYPE_NONE && + if (is_active_view && state && state->type != ui::TEXT_INPUT_TYPE_NONE && state->mode != ui::TEXT_INPUT_MODE_NONE) { #if !BUILDFLAG(IS_WIN) if (state->show_ime_if_needed && @@ -3718,7 +3725,7 @@ } // Ensure that selection bounds changes are sent to the IME. - if (state && state->type != ui::TEXT_INPUT_TYPE_NONE) { + if (state && state->type != ui::TEXT_INPUT_TYPE_NONE && is_active_view) { text_input_manager->NotifySelectionBoundsChanged(updated_view); if (!check_alive()) { return; @@ -3732,7 +3739,7 @@ // Monitor the composition information if there is a focused editable node. render_widget_host->RequestCompositionUpdates( false /* immediate_request */, - state && + is_active_view && state && (state->type != ui::TEXT_INPUT_TYPE_NONE) /* monitor_updates */); } } diff --git a/content/browser/renderer_host/render_widget_host_view_aura.h b/content/browser/renderer_host/render_widget_host_view_aura.h index d5a45aff..09955d4 100644 --- a/content/browser/renderer_host/render_widget_host_view_aura.h +++ b/content/browser/renderer_host/render_widget_host_view_aura.h @@ -881,6 +881,9 @@ ui::EventPointerType last_pointer_type_before_focus_ = ui::EventPointerType::kUnknown; + // Tracks the last text input type notified to the InputMethod. + ui::TextInputType last_text_input_type_ = ui::TEXT_INPUT_TYPE_NONE; + bool is_first_navigation_ = true; viz::LocalSurfaceId inset_surface_id_; diff --git a/content/browser/renderer_host/render_widget_host_view_mac.mm b/content/browser/renderer_host/render_widget_host_view_mac.mm index dd7e1ec2..31561fd 100644 --- a/content/browser/renderer_host/render_widget_host_view_mac.mm +++ b/content/browser/renderer_host/render_widget_host_view_mac.mm @@ -784,13 +784,16 @@ // Set the monitor state based on the text input focus state. const bool has_focus = HasFocus(); - bool need_monitor_composition = - has_focus && state && state->type != ui::TEXT_INPUT_TYPE_NONE; + const bool is_active_view = + text_input_manager->GetActiveWidget() && + text_input_manager->GetActiveWidget()->GetView() == updated_view; + bool need_monitor_composition = has_focus && is_active_view && state && + state->type != ui::TEXT_INPUT_TYPE_NONE; widget_host->RequestCompositionUpdates(false /* immediate_request */, need_monitor_composition); - if (has_focus) { + if (has_focus && is_active_view) { SetTextInputActive(true); // Let AppKit cache the new input context to make IMEs happy. diff --git a/content/browser/renderer_host/text_input_manager.cc b/content/browser/renderer_host/text_input_manager.cc index 90f7ffa6..ba78e15 100644 --- a/content/browser/renderer_host/text_input_manager.cc +++ b/content/browser/renderer_host/text_input_manager.cc @@ -12,6 +12,9 @@ #include "base/strings/string_number_conversions.h" #include "base/trace_event/trace_event.h" #include "build/build_config.h" +#include "content/browser/renderer_host/frame_tree.h" +#include "content/browser/renderer_host/frame_tree_node.h" +#include "content/browser/renderer_host/render_frame_host_impl.h" #include "content/browser/renderer_host/render_widget_host_impl.h" #include "content/browser/renderer_host/render_widget_host_view_base.h" #include "ui/base/ime/text_input_flags.h" @@ -274,17 +277,17 @@ } // If |view| is different from |active_view| and its |TextInputState.type| is - // not NONE, |active_view_| should change to |view|. + // not NONE, |active_view_| should change to |view| only if |view| has focus. if (text_input_state.type != ui::TEXT_INPUT_TYPE_NONE && - active_view_ != view) { + active_view_ != view && IsViewFocused(view)) { if (active_view_) { // Ideally, we should always receive an IPC from |active_view_|'s // RenderWidget to reset its |TextInputState.type| to NONE, before any // other RenderWidget updates its TextInputState. But there is no - // guarantee in the order of IPCs from different RenderWidgets and another - // RenderWidget's IPC might arrive sooner and we reach here. To make the - // IME behavior identical to the non-OOPIF case, we have to manually reset - // the state for |active_view_|. + // guarantee in the order of IPCs from different RenderWidgets and + // another RenderWidget's IPC might arrive sooner and we reach here. To + // make the IME behavior identical to the non-OOPIF case, we have to + // manually reset the state for |active_view_|. text_input_state_map_[active_view_]->type = ui::TEXT_INPUT_TYPE_NONE; RenderWidgetHostViewBase* active_view = active_view_; active_view_ = nullptr; @@ -293,10 +296,13 @@ active_view_ = view; } - // If the state for |active_view_| is none, then we no longer have an - // |active_view_|. - if (active_view_ == view && text_input_state.type == ui::TEXT_INPUT_TYPE_NONE) + // If the state for |active_view_| is none, or if |active_view_| is no longer + // focused, then we no longer have an |active_view_|. + if (active_view_ == view && + (text_input_state.type == ui::TEXT_INPUT_TYPE_NONE || + !IsViewFocused(view))) { active_view_ = nullptr; + } NotifyObserversAboutInputStateUpdate(view, changed); } @@ -501,6 +507,49 @@ return text_input_state_map_.count(view) == 1; } +bool TextInputManager::IsViewFocused(RenderWidgetHostViewBase* view) const { + if (!view || !view->host()) { + return false; + } + if (view->GetWidgetType() == WidgetType::kPopup) { + // Popups themselves do not hold frame focus directly. Instead, delegate the + // focus check to the frame that created the popup. + if (auto creator_id = view->host()->GetPopupCreatorFrameId()) { + RenderFrameHostImpl* creator_rfh = + RenderFrameHostImpl::FromID(*creator_id); + if (!creator_rfh) { + return false; + } + // When site isolation is disabled (such as for subframes on Android), + // child frames share the root RenderWidgetHostView with the main frame. + // Checking only whether the creator's view is focused would incorrectly + // return true even if the creator frame is not focused. Verify that the + // creator frame itself is the currently focused frame in the frame tree. + FrameTreeNode* focused_frame = + creator_rfh->frame_tree_node()->frame_tree().GetFocusedFrame(); + RenderFrameHostImpl* focused_rfh = + focused_frame ? focused_frame->current_frame_host() + : creator_rfh->GetMainFrame(); + if (creator_rfh != focused_rfh) { + return false; + } + RenderWidgetHostViewBase* creator_view = + static_cast<RenderWidgetHostViewBase*>(creator_rfh->GetView()); + return IsViewFocused(creator_view); + } + return false; + } + RenderWidgetHostViewBase* root_view = view->GetRootView(); + if (!root_view) { + return false; + } + RenderWidgetHostImpl* focused_widget = root_view->GetFocusedWidget(); + if (!focused_widget) { + return false; + } + return view->GetRenderWidgetHost() == focused_widget; +} + void TextInputManager::AddObserver(Observer* observer) { observer_list_.AddObserver(observer); } diff --git a/content/browser/renderer_host/text_input_manager.h b/content/browser/renderer_host/text_input_manager.h index 6e07e5e..61800e5 100644 --- a/content/browser/renderer_host/text_input_manager.h +++ b/content/browser/renderer_host/text_input_manager.h @@ -284,6 +284,11 @@ // Returns true if |view| is already registered. bool IsRegistered(RenderWidgetHostViewBase* view) const; + // Returns true if |view| is currently the focused view. For popup widgets, + // popups do not hold frame focus directly, so this check is performed + // against the focus state of the frame that created the popup. + bool IsViewFocused(RenderWidgetHostViewBase* view) const; + // Add and remove observers for notifications regarding updates in the // TextInputState. Clients must be sure to remove themselves before they go // away. diff --git a/chrome/browser/site_isolation/site_per_process_text_input_interactive_uitest.cc b/chrome/browser/site_isolation/site_per_process_text_input_interactive_uitest.cc index dc795c6a..ac1d15a 100644 --- a/chrome/browser/site_isolation/site_per_process_text_input_interactive_uitest.cc +++ b/chrome/browser/site_isolation/site_per_process_text_input_interactive_uitest.cc @@ -1425,4 +1425,41 @@ } #endif // defined(MAC_OSX) +// Test that an unfocused child frame cannot become the active view. +IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, + UnfocusedChildFrameCannotBecomeActive) { + CreateIframePage("a(b)"); + content::RenderFrameHost* main_frame = GetFrame(IndexVector{}); + content::RenderFrameHost* child_frame = GetFrame(IndexVector{0}); + + AddInputFieldToFrame(main_frame, "password", "secret", true); + AddInputFieldToFrame(child_frame, "text", "child", true); + + // Focus the input in the main frame. + content::TextInputManagerTypeObserver main_type_observer( + active_contents(), ui::TEXT_INPUT_TYPE_PASSWORD); + content::SimulateCharTyped(active_contents(), '\t'); + main_type_observer.Wait(); + + EXPECT_EQ(main_frame->GetView(), + content::GetActiveViewFromWebContents(active_contents())); + EXPECT_EQ(ui::TEXT_INPUT_TYPE_PASSWORD, + content::GetTextInputTypeFromWebContents(active_contents())); + + // Send a TextInputStateChanged update with a non-NONE type from the unfocused + // child frame. + auto modified_state = ui::mojom::TextInputState::New(); + modified_state->type = ui::TEXT_INPUT_TYPE_TEXT; + modified_state->mode = ui::TEXT_INPUT_MODE_DEFAULT; + content::SendTextInputStateChangedToWidget(child_frame->GetRenderWidgetHost(), + std::move(modified_state)); + + // The active view must remain the main frame view since the child frame is + // not focused. + EXPECT_EQ(main_frame->GetView(), + content::GetActiveViewFromWebContents(active_contents())); + EXPECT_EQ(ui::TEXT_INPUT_TYPE_PASSWORD, + content::GetTextInputTypeFromWebContents(active_contents())); +} + #endif // !BUILDFLAG(IS_ANDROID) diff --git a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc index b5a45ed..2c0661c 100644 --- a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc +++ b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc @@ -26,6 +26,7 @@ #include "base/test/bind.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/null_task_runner.h" +#include "base/test/run_until.h" #include "base/test/scoped_feature_list.h" #include "base/test/test_timeouts.h" #include "base/test/with_feature_override.h" @@ -733,10 +734,8 @@ ui::TextInputType type) { DCHECK_NE(ui::TEXT_INPUT_TYPE_NONE, type); // First mock-focus the widget if not already. - if (render_widget_host_delegate()->GetFocusedRenderWidgetHost( - widget_host_) != view->GetRenderWidgetHost()) { - render_widget_host_delegate()->set_focused_widget(view->host()); - } + render_widget_host_delegate()->set_focused_widget(view ? view->host() + : nullptr); TextInputManager* manager = GetTextInputManager(view); if (manager->GetActiveWidget()) { @@ -6503,6 +6502,43 @@ } } +// Test that an unfocused view cannot become active in TextInputManager. +TEST_F(InputMethodResultAuraTest, UnfocusedViewCannotBecomeActive) { + TextInputManager* manager = GetTextInputManager(tab_view()); + EXPECT_TRUE(manager); + + ActivateViewForTextInputManager(views_[0], ui::TEXT_INPUT_TYPE_TEXT); + EXPECT_EQ(views_[0], manager->active_view_for_testing()); + EXPECT_EQ(widget_hosts_[0], manager->GetActiveWidget()); + EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, tab_view()->GetTextInputType()); + + // Attempt to activate an unfocused view without granting focus. + ui::mojom::TextInputState child_state; + child_state.type = ui::TEXT_INPUT_TYPE_PASSWORD; + views_[1]->TextInputStateChanged(child_state); + + // The active view, active widget, and text input type must remain unchanged. + EXPECT_EQ(views_[0], manager->active_view_for_testing()); + EXPECT_EQ(widget_hosts_[0], manager->GetActiveWidget()); + EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, tab_view()->GetTextInputType()); + + // Inserting text should route to the focused widget (index 0), not widget 1. + text_input_client()->InsertText( + u"sample_text", + ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); + std::string dispatched_messages; + EXPECT_TRUE(base::test::RunUntil([&]() { + dispatched_messages = GetMessageNames( + widget_hosts_[0]->input_handler()->GetAndResetDispatchedMessages()); + return !dispatched_messages.empty(); + })); + EXPECT_EQ("CommitText", dispatched_messages); + EXPECT_EQ( + "", + GetMessageNames( + widget_hosts_[1]->input_handler()->GetAndResetDispatchedMessages())); +} + // A class of tests which verify the correctness of some tracked IME related // state at the browser side. Each test verifies the correctness tracking for // one specific state. To do so, the views are activated in a predetermined @@ -6719,22 +6755,24 @@ ASSERT_TRUE(manager); // Send an autocorrect span from a child view, making it the active view. + ActivateViewForTextInputManager(views_[1], ui::TEXT_INPUT_TYPE_TEXT); ui::mojom::TextInputState child_state; child_state.type = ui::TEXT_INPUT_TYPE_TEXT; child_state.ime_text_spans_info.push_back(ui::mojom::ImeTextSpanInfo::New( ui::ImeTextSpan(ui::ImeTextSpan::Type::kAutocorrect, 0, 1000), gfx::Rect())); views_[1]->TextInputStateChanged(child_state); - ASSERT_EQ(views_[1], manager->active_view_for_testing()); + EXPECT_EQ(views_[1], manager->active_view_for_testing()); EXPECT_EQ(gfx::Range(0, 1000), manager->GetAutocorrectRange()); // Activate the tab view with no autocorrect span. The child view's span is // still stored in the manager, but it must not be returned for the now // active tab view. + ActivateViewForTextInputManager(views_[0], ui::TEXT_INPUT_TYPE_TEXT); ui::mojom::TextInputState tab_state; tab_state.type = ui::TEXT_INPUT_TYPE_TEXT; views_[0]->TextInputStateChanged(tab_state); - ASSERT_EQ(views_[0], manager->active_view_for_testing()); + EXPECT_EQ(views_[0], manager->active_view_for_testing()); EXPECT_EQ(gfx::Range(), manager->GetAutocorrectRange()); ... (file diff truncated)
Regression Test / PoC
diff --git a/chrome/browser/site_isolation/site_per_process_text_input_interactive_uitest.cc b/chrome/browser/site_isolation/site_per_process_text_input_interactive_uitest.cc
index dc795c6a..ac1d15a 100644
--- a/chrome/browser/site_isolation/site_per_process_text_input_interactive_uitest.cc
+++ b/chrome/browser/site_isolation/site_per_process_text_input_interactive_uitest.cc
@@ -1425,4 +1425,41 @@
}
#endif // defined(MAC_OSX)
+// Test that an unfocused child frame cannot become the active view.
+IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest,
+ UnfocusedChildFrameCannotBecomeActive) {
+ CreateIframePage("a(b)");
+ content::RenderFrameHost* main_frame = GetFrame(IndexVector{});
+ content::RenderFrameHost* child_frame = GetFrame(IndexVector{0});
+
+ AddInputFieldToFrame(main_frame, "password", "secret", true);
+ AddInputFieldToFrame(child_frame, "text", "child", true);
+
+ // Focus the input in the main frame.
+ content::TextInputManagerTypeObserver main_type_observer(
+ active_contents(), ui::TEXT_INPUT_TYPE_PASSWORD);
+ content::SimulateCharTyped(active_contents(), '\t');
+ main_type_observer.Wait();
+
+ EXPECT_EQ(main_frame->GetView(),
+ content::GetActiveViewFromWebContents(active_contents()));
+ EXPECT_EQ(ui::TEXT_INPUT_TYPE_PASSWORD,
+ content::GetTextInputTypeFromWebContents(active_contents()));
+
+ // Send a TextInputStateChanged update with a non-NONE type from the unfocused
+ // child frame.
+ auto modified_state = ui::mojom::TextInputState::New();
+ modified_state->type = ui::TEXT_INPUT_TYPE_TEXT;
+ modified_state->mode = ui::TEXT_INPUT_MODE_DEFAULT;
+ content::SendTextInputStateChangedToWidget(child_frame->GetRenderWidgetHost(),
+ std::move(modified_state));
+
+ // The active view must remain the main frame view since the child frame is
+ // not focused.
+ EXPECT_EQ(main_frame->GetView(),
+ content::GetActiveViewFromWebContents(active_contents()));
+ EXPECT_EQ(ui::TEXT_INPUT_TYPE_PASSWORD,
+ content::GetTextInputTypeFromWebContents(active_contents()));
+}
+
#endif // !BUILDFLAG(IS_ANDROID)
diff --git a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
index b5a45ed..2c0661c 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
@@ -26,6 +26,7 @@
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/null_task_runner.h"
+#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
#include "base/test/with_feature_override.h"
@@ -733,10 +734,8 @@
ui::TextInputType type) {
DCHECK_NE(ui::TEXT_INPUT_TYPE_NONE, type);
// First mock-focus the widget if not already.
- if (render_widget_host_delegate()->GetFocusedRenderWidgetHost(
- widget_host_) != view->GetRenderWidgetHost()) {
- render_widget_host_delegate()->set_focused_widget(view->host());
- }
+ render_widget_host_delegate()->set_focused_widget(view ? view->host()
+ : nullptr);
TextInputManager* manager = GetTextInputManager(view);
if (manager->GetActiveWidget()) {
@@ -6503,6 +6502,43 @@
}
}
+// Test that an unfocused view cannot become active in TextInputManager.
+TEST_F(InputMethodResultAuraTest, UnfocusedViewCannotBecomeActive) {
+ TextInputManager* manager = GetTextInputManager(tab_view());
+ EXPECT_TRUE(manager);
+
+ ActivateViewForTextInputManager(views_[0], ui::TEXT_INPUT_TYPE_TEXT);
+ EXPECT_EQ(views_[0], manager->active_view_for_testing());
+ EXPECT_EQ(widget_hosts_[0], manager->GetActiveWidget());
+ EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, tab_view()->GetTextInputType());
+
+ // Attempt to activate an unfocused view without granting focus.
+ ui::mojom::TextInputState child_state;
+ child_state.type = ui::TEXT_INPUT_TYPE_PASSWORD;
+ views_[1]->TextInputStateChanged(child_state);
+
+ // The active view, active widget, and text input type must remain unchanged.
+ EXPECT_EQ(views_[0], manager->active_view_for_testing());
+ EXPECT_EQ(widget_hosts_[0], manager->GetActiveWidget());
+ EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, tab_view()->GetTextInputType());
+
+ // Inserting text should route to the focused widget (index 0), not widget 1.
+ text_input_client()->InsertText(
+ u"sample_text",
+ ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
+ std::string dispatched_messages;
+ EXPECT_TRUE(base::test::RunUntil([&]() {
+ dispatched_messages = GetMessageNames(
+ widget_hosts_[0]->input_handler()->GetAndResetDispatchedMessages());
+ return !dispatched_messages.empty();
+ }));
+ EXPECT_EQ("CommitText", dispatched_messages);
+ EXPECT_EQ(
+ "",
+ GetMessageNames(
+ widget_hosts_[1]->input_handler()->GetAndResetDispatchedMessages()));
+}
+
// A class of tests which verify the correctness of some tracked IME related
// state at the browser side. Each test verifies the correctness tracking for
// one specific state. To do so, the views are activated in a predetermined
@@ -6719,22 +6755,24 @@
ASSERT_TRUE(manager);
// Send an autocorrect span from a child view, making it the active view.
+ ActivateViewForTextInputManager(views_[1], ui::TEXT_INPUT_TYPE_TEXT);
ui::mojom::TextInputState child_state;
child_state.type = ui::TEXT_INPUT_TYPE_TEXT;
child_state.ime_text_spans_info.push_back(ui::mojom::ImeTextSpanInfo::New(
ui::ImeTextSpan(ui::ImeTextSpan::Type::kAutocorrect, 0, 1000),
gfx::Rect()));
views_[1]->TextInputStateChanged(child_state);
- ASSERT_EQ(views_[1], manager->active_view_for_testing());
+ EXPECT_EQ(views_[1], manager->active_view_for_testing());
EXPECT_EQ(gfx::Range(0, 1000), manager->GetAutocorrectRange());
// Activate the tab view with no autocorrect span. The child view's span is
// still stored in the manager, but it must not be returned for the now
// active tab view.
+ ActivateViewForTextInputManager(views_[0], ui::TEXT_INPUT_TYPE_TEXT);
ui::mojom::TextInputState tab_state;
tab_state.type = ui::TEXT_INPUT_TYPE_TEXT;
views_[0]->TextInputStateChanged(tab_state);
- ASSERT_EQ(views_[0], manager->active_view_for_testing());
+ EXPECT_EQ(views_[0], manager->active_view_for_testing());
EXPECT_EQ(gfx::Range(), manager->GetAutocorrectRange());
// Activate the tab view with its own autocorrect span and verify that the
diff --git a/content/browser/renderer_host/render_widget_host_view_mac_unittest.mm b/content/browser/renderer_host/render_widget_host_view_mac_unittest.mm
index 8858d55..29d1191e5 100644
--- a/content/browser/renderer_host/render_widget_host_view_mac_unittest.mm
+++ b/content/browser/renderer_host/render_widget_host_view_mac_unittest.mm
@@ -599,6 +599,9 @@
void ActivateViewWithTextInputManager(RenderWidgetHostViewBase* view,
ui::TextInputType type) {
+ if (view && view->host()) {
+ delegate_.set_focused_widget(view->host());
+ }
ui::mojom::TextInputState state;
state.type = type;
view->TextInputStateChanged(state);
@@ -2000,6 +2003,9 @@
void SetTextInputType(RenderWidgetHostViewBase* view,
ui::TextInputType type,
ui::TextInputFlags flags = ui::TEXT_INPUT_FLAG_NONE) {
+ if (view && view->host()) {
+ delegate_.set_focused_widget(view->host());
+ }
ui::mojom::TextInputState state;
state.type = type;
state.flags = flags;
@@ -2413,7 +2419,7 @@
state.type = ui::TEXT_INPUT_TYPE_TEXT;
// Make the tab's widget active.
- tab_view()->TextInputStateChanged(state);
+ SetTextInputType(tab_view(), ui::TEXT_INPUT_TYPE_TEXT);
base::RunLoop().RunUntilIdle();
MockWidgetInputHandler::MessageVector events =
@@ -2428,7 +2434,7 @@
EXPECT_TRUE(message->monitor_request());
// Now make the child view active.
- child_view_->TextInputStateChanged(state);
+ SetTextInputType(child_view_, ui::TEXT_INPUT_TYPE_TEXT);
// The tab should receive another IPC for composition updates.
base::RunLoop().RunUntilIdle();
@@ -2452,7 +2458,7 @@
EXPECT_TRUE(message->monitor_request());
// Make the tab view active again.
- tab_view()->TextInputStateChanged(state);
+ SetTextInputType(tab_view(), ui::TEXT_INPUT_TYPE_TEXT);
base::RunLoop().RunUntilIdle();
events = child_widget_->GetAndResetDispatchedMessages();
diff --git a/content/browser/renderer_host/text_input_manager_unittest.cc b/content/browser/renderer_host/text_input_manager_unittest.cc
index cc9ba489..f51139b5 100644
--- a/content/browser/renderer_host/text_input_manager_unittest.cc
+++ b/content/browser/renderer_host/text_input_manager_unittest.cc
@@ -4,8 +4,16 @@
#include "content/browser/renderer_host/text_input_manager.h"
+#include "base/command_line.h"
#include "build/build_config.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
+#include "content/browser/renderer_host/render_widget_host_factory.h"
+#include "content/browser/renderer_host/render_widget_host_impl.h"
+#include "content/browser/site_instance_group.h"
+#include "content/browser/site_instance_impl.h"
+#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
+#include "content/public/test/test_utils.h"
#include "content/test/test_render_view_host.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/ime/text_input_flags.h"
@@ -16,7 +24,9 @@
class TextInputManagerTest : public RenderViewHostTestHarness {
public:
- TextInputManagerTest() = default;
+ TextInputManagerTest() {
+ IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());
+ }
~TextInputManagerTest() override = default;
};
@@ -222,4 +232,130 @@
EXPECT_EQ(region->bounding_box, bounding_box);
}
+// Test that an unfocused child frame cannot become the active view.
+TEST_F(TextInputManagerTest, UnfocusedChildFrameCannotBecomeActive) {
+ GURL parent_url("https://a.test/");
+ NavigationSimulator::CreateRendererInitiated(parent_url, main_rfh())
+ ->Commit();
+
+ auto* child_rfh =
+ content::RenderFrameHostTester::For(main_rfh())->AppendChild("child");
+ GURL child_url("https://b.test/");
+ child_rfh =
+ NavigationSimulator::NavigateAndCommitFromDocument(child_url, child_rfh);
+
+ auto* main_view = static_cast<RenderWidgetHostViewBase*>(
+ main_rfh()->GetRenderWidgetHost()->GetView());
+ auto* child_view = static_cast<RenderWidgetHostViewBase*>(
+ child_rfh->GetRenderWidgetHost()->GetView());
+
+ TextInputManager* manager = main_view->GetTextInputManager();
+ EXPECT_TRUE(manager);
+
+ // Main frame is focused. Activate text input on main frame.
+ ui::mojom::TextInputState main_state;
+ main_state.type = ui::TEXT_INPUT_TYPE_PASSWORD;
+ manager->UpdateTextInputState(main_view, main_state);
+
+ EXPECT_EQ(main_view, manager->active_view_for_testing());
+ EXPECT_EQ(main_rfh()->GetRenderWidgetHost(), manager->GetActiveWidget());
+
+ // Attempt to activate text input from unfocused child frame.
+ ui::mojom::TextInputState child_state;
+ child_state.type = ui::TEXT_INPUT_TYPE_TEXT;
+ manager->UpdateTextInputState(child_view, child_state);
+
+ // Active view and widget must remain unchanged (main frame).
+ EXPECT_EQ(main_view, manager->active_view_for_testing());
+ EXPECT_EQ(main_rfh()->GetRenderWidgetHost(), manager->GetActiveWidget());
+}
+
+// Test that a popup widget's ability to become active depends on whether
+// its creator frame is focused.
+TEST_F(TextInputManagerTest, PopupActiveStateDependsOnCreatorFrameFocus) {
+ GURL parent_url("https://a.test/");
+ NavigationSimulator::CreateRendererInitiated(parent_url, main_rfh())
+ ->Commit();
+
+ auto* child_rfh =
+ content::RenderFrameHostTester::For(main_rfh())->AppendChild("child");
+ GURL child_url("https://b.test/");
+ child_rfh =
+ NavigationSimulator::NavigateAndCommitFromDocument(child_url, child_rfh);
+
+ auto* main_view = static_cast<RenderWidgetHostViewBase*>(
+ main_rfh()->GetRenderWidgetHost()->GetView());
+ TextInputManager* manager = main_view->GetTextInputManager();
+ EXPECT_TRUE(manager);
+
+ RenderFrameHostImpl* main_rfh_impl =
+ static_cast<RenderFrameHostImpl*>(main_rfh());
+ RenderFrameHostImpl* child_rfh_impl =
+ static_cast<RenderFrameHostImpl*>(child_rfh);
+
+ // 1. Create a popup widget view and associate its creator frame with the main
+ // frame (which currently has focus).
+ auto popup_widget = RenderWidgetHostFactory::Create(
+ /*frame_tree=*/nullptr, main_rfh_impl->GetRenderWidgetHost()->delegate(),
+ viz::FrameSinkId(1, 1),
+ main_rfh_impl->GetSiteInstance()->group()->GetSafeRef(),
+ main_rfh()->GetProcess()->GetNextRoutingID(),
+ /*hidden=*/false,
+ /*renderer_initiated_creation=*/false);
+ auto popup_view =
... (truncated)
Original Bug Report
Site Isolation bypass: Cross-origin IME keystroke theft via unvalidated TextInputStateChanged
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A logic flaw in TextInputManager allows a compromised cross-origin renderer to hijack the active text input view by spoofing WidgetHost.TextInputStateChanged IPCs. Because the browser does not verify if the sender actually holds frame focus, IME composition events (like dictation or CJK input) are misrouted to the attacker. This potentially allows an attacker to steal sensitive user input, such as passwords, across origin boundaries.
Affected files:
content/browser/renderer_host/text_input_manager.cccontent/browser/renderer_host/render_widget_host_view_aura.cccontent/browser/renderer_host/render_widget_host_view_mac.mmcontent/browser/renderer_host/render_widget_host_impl.cc
Estimated timestamp from git blame: 2020-08-05
Summary
A potential vulnerability in TextInputManager::UpdateTextInputState allows a compromised renderer (such as an out-of-process iframe) to hijack the active_view_ pointer. This leads to the cross-origin theft of IME-composed keystrokes, completely bypassing Site Isolation.
While the browser correctly tracks focus for raw keyboard events, it relies on an unvalidated state update for routing IME-specific operations like text commitment and composition updates.
Technical Details
In content/browser/renderer_host/text_input_manager.cc, the method UpdateTextInputState is responsible for updating the active_view_. This view determines which renderer receives IME output from the OS. The logic currently assigns active_view_ to any RenderWidgetHostView that sends a WidgetHost.TextInputStateChanged IPC with a text input type other than NONE:
// content/browser/renderer_host/text_input_manager.cc
if (text_input_state.type != ui::TEXT_INPUT_TYPE_NONE &&
active_view_ != view) {
if (active_view_) {
// ... resets previous active_view_ state ...
}
active_view_ = view;
}
There is no cross-check against the browser’s actual focus state (e.g., via FrameTree or WebContentsImpl::GetFocusedRenderWidgetHost()). Consequently, a compromised cross-origin OOPIF (Out-of-Process iframe) can repeatedly send spoofed TextInputStateChanged IPCs to hijack active_view_ while the user is actively interacting with a focused frame on a different origin.
Subsequent IME operations—such as InsertText (which triggers ImeCommitText), SetCompositionText, and ConfirmCompositionText—route their events through TextInputManager::GetActiveWidget(). On platforms using Aura (Windows, Linux, ChromeOS) and macOS, these events are misrouted from the top-level view (like RenderWidgetHostViewAura) to the attacker’s renderer.
This behavior contrasts with raw key events and other operations like ExtendSelectionAndDelete, which correctly verify focus and are therefore not susceptible to this hijack.
Impact
This is a potential Site Isolation bypass that allows a compromised renderer to steal user input from other origins. The impact is specifically high for users of CJK Input Method Editors (IMEs), dead-key accents, dictation, and on-screen keyboards, where input is buffered and committed via the affected code paths.
Suggested Reproduction Steps
Note: Our tooling agent does not yet have the ability to run code, so these are potential steps an attacker would follow to trigger the vulnerability.
- Navigate to a legitimate page on Origin A containing a sensitive input field (e.g., a password field) and embed a cross-origin iframe from Origin B.
- Compromise the Origin B renderer process.
- Have the compromised Origin B renderer periodically send
blink.mojom.WidgetHost.TextInputStateChangedMojo IPCs withstate.type = TEXT_INPUT_TYPE_TEXT. - As a user, focus the input field on Origin A and begin typing using an IME (e.g., Japanese IME or Windows Dictation).
- Observe in the browser process that
TextInputManager::active_view_is pointed to Origin B’s view even though Origin A’s frame remains focused. - The OS IME composition events (e.g.,
RenderWidgetHostViewAura::InsertText) will retrieve the active widget viatext_input_manager_->GetActiveWidget(). RenderWidgetHostImpl::ImeCommitTextwill send the composed text via IPC to Origin B’s renderer, where the attacker can exfiltrate it.
Suggested Fix
TextInputManager::UpdateTextInputState (or the callers in RenderWidgetHostImpl/RenderWidgetHostViewBase) must validate that the RenderWidgetHostViewBase sending the IPC corresponds to the currently focused frame before updating active_view_. This can be achieved by checking against WebContentsImpl::GetFocusedRenderWidgetHost() or verifying the focus state within the FrameTree. If a background or unfocused frame sends a TextInputStateChanged IPC with a non-NONE type, the browser should either ignore the update to active_view_ or terminate the misbehaving renderer.
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.