Overview

High
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
Tracker514009323
Fix commitdac82deaf7c9 (chromium/src) +151/-8
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
TEST_F
content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
modified
for
content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
modified
if
content/browser/renderer_host/text_input_manager.cc
modified
TextInputManagerTest
content/browser/renderer_host/text_input_manager_unittest.cc
modified
TEST_F
content/browser/renderer_host/text_input_manager_unittest.cc
modified

Files Changed

  • content/browser/renderer_host/render_widget_host_view_aura_unittest.cc
  • content/browser/renderer_host/text_input_manager.cc
  • content/browser/renderer_host/text_input_manager_unittest.cc
From dac82deaf7c97078dc1363771aba2a657e74b804 Mon Sep 17 00:00:00 2001
From: Jonathan Ross <[email protected]>
Date: Mon, 01 Jun 2026 10:56:14 -0700
Subject: [PATCH] Clamp EditContext bounds to visible viewport in TextInputManager.

This prevents sites from positioning IME candidate windows outside the
web content area.

Bug: 514009323
Change-Id: I92ba2a32efc620b2d77c512b915efd3af5b98ad8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7871130
Reviewed-by: Kartar Singh <[email protected]>
Commit-Queue: Jonathan Ross <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1639519}
---

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 f4d704e..a7087ef0 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
@@ -6510,6 +6510,35 @@
     EXPECT_EQ(measured_rect, text_input_client()->GetCaretBounds());
   }
 }
+TEST_F(InputMethodStateAuraTest, EditContextBoundsClamped) {
+  RenderWidgetHostViewAura* view = tab_view();
+  view->SetBounds(gfx::Rect(0, 0, 800, 600));
+  ActivateViewForTextInputManager(view, ui::TEXT_INPUT_TYPE_TEXT);
+  TextInputManager* manager = GetTextInputManager(view);
+
+  ui::mojom::TextInputState state;
+  state.type = ui::TEXT_INPUT_TYPE_TEXT;
+  state.edit_context_control_bounds = gfx::Rect(-50, -50, 100, 100);
+  state.edit_context_selection_bounds = gfx::Rect(-50, -50, 100, 100);
+
+  manager->UpdateTextInputState(view, state);
+
+  std::optional<gfx::Rect> control_bounds = manager->GetTextControlBounds();
+  std::optional<gfx::Rect> selection_bounds = manager->GetTextSelectionBounds();
+
+  EXPECT_TRUE(control_bounds.has_value());
+  EXPECT_TRUE(selection_bounds.has_value());
+
+  // Expected adjusted bounds in view local space is (0, 0, 100, 100).
+  gfx::Rect expected_local_bounds = gfx::Rect(0, 0, 100, 100);
+
+  gfx::Rect expected_bounds = gfx::Rect(
+      view->TransformPointToRootCoordSpace(expected_local_bounds.origin()),
+      expected_local_bounds.size());
+
+  EXPECT_EQ(control_bounds.value(), expected_bounds);
+  EXPECT_EQ(selection_bounds.value(), expected_bounds);
+}
 
 // This test is for composition character bounds.
 TEST_F(InputMethodStateAuraTest, GetCompositionCharacterBounds) {
@@ -6518,6 +6547,7 @@
   EXPECT_FALSE(text_input_client()->GetCompositionCharacterBounds(0, &bound));
   for (auto index : active_view_sequence_) {
     ActivateViewForTextInputManager(views_[index], ui::TEXT_INPUT_TYPE_TEXT);
+    views_[index]->SetBounds(gfx::Rect(0, 0, 800, 600));
     // Simulate an IPC to set character bounds for the view.
     views_[index]->ImeCompositionRangeChanged(
         gfx::Range(), {{gfx::Rect(1, 2, 3, 4 + index)}});
diff --git a/content/browser/renderer_host/text_input_manager.cc b/content/browser/renderer_host/text_input_manager.cc
index 86dff7f9..a6fd59d 100644
--- a/content/browser/renderer_host/text_input_manager.cc
+++ b/content/browser/renderer_host/text_input_manager.cc
@@ -41,6 +41,19 @@
 #endif
 }
 
+// We want to validate with the viewport's rect. However this lookup can be
+// invoked on a `RenderWidgetHostViewChildFrame` which has been disconnected
+// from the viewport. In such a case we return the requested size of the child
+// view
+gfx::Rect GetViewportRect(RenderWidgetHostViewBase* view) {
+  auto* root_view = view->GetRootView();
+  if (root_view) {
+    return gfx::Rect(root_view->GetVisibleViewportSize());
+  } else {
+    return gfx::Rect(view->GetRequestedRendererSize());
+  }
+}
+
 }  // namespace
 
 TextInputManager::TextInputManager() : active_view_(nullptr) {}
@@ -161,8 +174,9 @@
   auto control_bounds = state->edit_context_control_bounds.value();
   auto new_top_left =
       active_view_->TransformPointToRootCoordSpace(control_bounds.origin());
-  return std::optional<gfx::Rect>(
-      gfx::Rect(new_top_left, control_bounds.size()));
+  control_bounds.set_origin(new_top_left);
+  control_bounds.AdjustToFit(GetViewportRect(active_view_));
+  return control_bounds;
 }
 
 const std::optional<gfx::Rect> TextInputManager::GetTextSelectionBounds()
@@ -174,8 +188,9 @@
   auto selection_bounds = state->edit_context_selection_bounds.value();
   auto new_top_left =
       active_view_->TransformPointToRootCoordSpace(selection_bounds.origin());
-  return std::optional<gfx::Rect>(
-      gfx::Rect(new_top_left, selection_bounds.size()));
+  selection_bounds.set_origin(new_top_left);
+  selection_bounds.AdjustToFit(GetViewportRect(active_view_));
+  return selection_bounds;
 }
 
 void TextInputManager::UpdateTextInputState(
@@ -383,11 +398,16 @@
   if (character_bounds.has_value()) {
     composition_range_info_map_[view].character_bounds.clear();
 
+    gfx::Rect viewport_rect = GetViewportRect(view);
     // The values for the bounds should be converted to root view's coordinates
     // before being stored.
     for (auto& rect : character_bounds.value()) {
+      gfx::Rect clamped_rect = rect;
+      clamped_rect.set_origin(
+          view->TransformPointToRootCoordSpace(clamped_rect.origin()));
+      clamped_rect.AdjustToFit(viewport_rect);
       composition_range_info_map_[view].character_bounds.emplace_back(
-          view->TransformPointToRootCoordSpace(rect.origin()), rect.size());
+          clamped_rect);
     }
 
     composition_range_info_map_[view].range.set_start(range.start());
diff --git a/content/browser/renderer_host/text_input_manager_unittest.cc b/content/browser/renderer_host/text_input_manager_unittest.cc
new file mode 100644
index 0000000..2786c38
--- /dev/null
+++ b/content/browser/renderer_host/text_input_manager_unittest.cc
@@ -0,0 +1,82 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/renderer_host/text_input_manager.h"
+
+#include "content/public/test/test_renderer_host.h"
+#include "content/test/test_render_view_host.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/range/range.h"
+
+namespace content {
+
+class TextInputManagerTest : public RenderViewHostTestHarness {
+ public:
+  TextInputManagerTest() = default;
+  ~TextInputManagerTest() override = default;
+};
+
+// Test that ImeCompositionRangeChanged clamps out-of-bounds character bounds.
+TEST_F(TextInputManagerTest, ImeCompositionRangeChanged_Clamped) {
+  RenderWidgetHostViewBase* view =
+      static_cast<RenderWidgetHostViewBase*>(rvh()->GetWidget()->GetView());
+
+  TextInputManager* manager = view->GetTextInputManager();
+  ASSERT_TRUE(manager);
+
+  // Make the view active in TextInputManager.
+  ui::mojom::TextInputState state;
+  state.type = ui::TEXT_INPUT_TYPE_TEXT;
+  manager->UpdateTextInputState(view, state);
+
+  view->SetBounds(gfx::Rect(0, 0, 800, 600));
+
+  // Simulate an IPC to set character bounds that are out of bounds (negative
+  // origin). Rect: x=-10, y=-10, w=50, h=50
+  manager->ImeCompositionRangeChanged(view, gfx::Range(0, 1),
+                                      {{gfx::Rect(-10, -10, 50, 50)}});
+
+  const TextInputManager::CompositionRangeInfo* info =
+      manager->GetCompositionRangeInfo();
+
+  ASSERT_TRUE(info);
+  ASSERT_EQ(info->character_bounds.size(), 1u);
+
+  // Should be clamped to fit in (0, 0, 800, 600) -> (0, 0, 50, 50)
+  gfx::Rect expected_bounds(0, 0, 50, 50);
+  EXPECT_EQ(info->character_bounds[0], expected_bounds);
+}
+
+// Test that ImeCompositionRangeChanged does not clamp in-bounds character
+// bounds.
+TEST_F(TextInputManagerTest, ImeCompositionRangeChanged_InBounds) {
+  RenderWidgetHostViewBase* view =
+      static_cast<RenderWidgetHostViewBase*>(rvh()->GetWidget()->GetView());
+
+  TextInputManager* manager = view->GetTextInputManager();
+  ASSERT_TRUE(manager);
+
+  // Make the view active in TextInputManager.
+  ui::mojom::TextInputState state;
+  state.type = ui::TEXT_INPUT_TYPE_TEXT;
+  manager->UpdateTextInputState(view, state);
Loading diff…

Regression Test / PoC

shipped with the fix
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 f4d704e..a7087ef0 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
@@ -6510,6 +6510,35 @@
     EXPECT_EQ(measured_rect, text_input_client()->GetCaretBounds());
   }
 }
+TEST_F(InputMethodStateAuraTest, EditContextBoundsClamped) {
+  RenderWidgetHostViewAura* view = tab_view();
+  view->SetBounds(gfx::Rect(0, 0, 800, 600));
+  ActivateViewForTextInputManager(view, ui::TEXT_INPUT_TYPE_TEXT);
+  TextInputManager* manager = GetTextInputManager(view);
+
+  ui::mojom::TextInputState state;
+  state.type = ui::TEXT_INPUT_TYPE_TEXT;
+  state.edit_context_control_bounds = gfx::Rect(-50, -50, 100, 100);
+  state.edit_context_selection_bounds = gfx::Rect(-50, -50, 100, 100);
+
+  manager->UpdateTextInputState(view, state);
+
+  std::optional<gfx::Rect> control_bounds = manager->GetTextControlBounds();
+  std::optional<gfx::Rect> selection_bounds = manager->GetTextSelectionBounds();
+
+  EXPECT_TRUE(control_bounds.has_value());
+  EXPECT_TRUE(selection_bounds.has_value());
+
+  // Expected adjusted bounds in view local space is (0, 0, 100, 100).
+  gfx::Rect expected_local_bounds = gfx::Rect(0, 0, 100, 100);
+
+  gfx::Rect expected_bounds = gfx::Rect(
+      view->TransformPointToRootCoordSpace(expected_local_bounds.origin()),
+      expected_local_bounds.size());
+
+  EXPECT_EQ(control_bounds.value(), expected_bounds);
+  EXPECT_EQ(selection_bounds.value(), expected_bounds);
+}
 
 // This test is for composition character bounds.
 TEST_F(InputMethodStateAuraTest, GetCompositionCharacterBounds) {
@@ -6518,6 +6547,7 @@
   EXPECT_FALSE(text_input_client()->GetCompositionCharacterBounds(0, &bound));
   for (auto index : active_view_sequence_) {
     ActivateViewForTextInputManager(views_[index], ui::TEXT_INPUT_TYPE_TEXT);
+    views_[index]->SetBounds(gfx::Rect(0, 0, 800, 600));
     // Simulate an IPC to set character bounds for the view.
     views_[index]->ImeCompositionRangeChanged(
         gfx::Range(), {{gfx::Rect(1, 2, 3, 4 + index)}});
diff --git a/content/browser/renderer_host/text_input_manager_unittest.cc b/content/browser/renderer_host/text_input_manager_unittest.cc
new file mode 100644
index 0000000..2786c38
--- /dev/null
+++ b/content/browser/renderer_host/text_input_manager_unittest.cc
@@ -0,0 +1,82 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/renderer_host/text_input_manager.h"
+
+#include "content/public/test/test_renderer_host.h"
+#include "content/test/test_render_view_host.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/range/range.h"
+
+namespace content {
+
+class TextInputManagerTest : public RenderViewHostTestHarness {
+ public:
+  TextInputManagerTest() = default;
+  ~TextInputManagerTest() override = default;
+};
+
+// Test that ImeCompositionRangeChanged clamps out-of-bounds character bounds.
+TEST_F(TextInputManagerTest, ImeCompositionRangeChanged_Clamped) {
+  RenderWidgetHostViewBase* view =
+      static_cast<RenderWidgetHostViewBase*>(rvh()->GetWidget()->GetView());
+
+  TextInputManager* manager = view->GetTextInputManager();
+  ASSERT_TRUE(manager);
+
+  // Make the view active in TextInputManager.
+  ui::mojom::TextInputState state;
+  state.type = ui::TEXT_INPUT_TYPE_TEXT;
+  manager->UpdateTextInputState(view, state);
+
+  view->SetBounds(gfx::Rect(0, 0, 800, 600));
+
+  // Simulate an IPC to set character bounds that are out of bounds (negative
+  // origin). Rect: x=-10, y=-10, w=50, h=50
+  manager->ImeCompositionRangeChanged(view, gfx::Range(0, 1),
+                                      {{gfx::Rect(-10, -10, 50, 50)}});
+
+  const TextInputManager::CompositionRangeInfo* info =
+      manager->GetCompositionRangeInfo();
+
+  ASSERT_TRUE(info);
+  ASSERT_EQ(info->character_bounds.size(), 1u);
+
+  // Should be clamped to fit in (0, 0, 800, 600) -> (0, 0, 50, 50)
+  gfx::Rect expected_bounds(0, 0, 50, 50);
+  EXPECT_EQ(info->character_bounds[0], expected_bounds);
+}
+
+// Test that ImeCompositionRangeChanged does not clamp in-bounds character
+// bounds.
+TEST_F(TextInputManagerTest, ImeCompositionRangeChanged_InBounds) {
+  RenderWidgetHostViewBase* view =
+      static_cast<RenderWidgetHostViewBase*>(rvh()->GetWidget()->GetView());
+
+  TextInputManager* manager = view->GetTextInputManager();
+  ASSERT_TRUE(manager);
+
+  // Make the view active in TextInputManager.
+  ui::mojom::TextInputState state;
+  state.type = ui::TEXT_INPUT_TYPE_TEXT;
+  manager->UpdateTextInputState(view, state);
+
+  view->SetBounds(gfx::Rect(0, 0, 800, 600));
+
+  // In bounds rect: x=10, y=10, w=50, h=50
+  manager->ImeCompositionRangeChanged(view, gfx::Range(0, 1),
+                                      {{gfx::Rect(10, 10, 50, 50)}});
+
+  const TextInputManager::CompositionRangeInfo* info =
+      manager->GetCompositionRangeInfo();
+
+  ASSERT_TRUE(info);
+  ASSERT_EQ(info->character_bounds.size(), 1u);
+
+  gfx::Rect expected_bounds(10, 10, 50, 50);
+  EXPECT_EQ(info->character_bounds[0], expected_bounds);
+}
+
+}  // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index adf9fc4..e9cc0c3 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2962,6 +2962,7 @@
     "../browser/renderer_host/render_widget_host_view_aura_unittest.cc",
     "../browser/renderer_host/render_widget_host_view_base_unittest.cc",
     "../browser/renderer_host/render_widget_host_view_child_frame_unittest.cc",
+    "../browser/renderer_host/text_input_manager_unittest.cc",
     "../browser/renderer_host/transient_allow_popup_unittest.cc",
     "../browser/renderer_host/visible_time_request_trigger_unittest.cc",
     "../browser/sandboxed_opaque_origin_creator_unittest.cc",
diff --git a/content/test/test_render_view_host.cc b/content/test/test_render_view_host.cc
index bc143b6..8989e9e 100644
--- a/content/test/test_render_view_host.cc
+++ b/content/test/test_render_view_host.cc
@@ -168,8 +168,16 @@
   delete this;
 }
 
+void TestRenderWidgetHostView::SetSize(const gfx::Size& size) {
+  bounds_.set_size(size);
+}
+
+void TestRenderWidgetHostView::SetBounds(const gfx::Rect& rect) {
+  bounds_ = rect;
+}
+
 gfx::Rect TestRenderWidgetHostView::GetViewBounds() {
-  return gfx::Rect();
+  return bounds_;
 }
 
 #if BUILDFLAG(IS_MAC)
diff --git a/content/test/test_render_view_host.h b/content/test/test_render_view_host.h
index 2a17a0f6..3986adfb 100644
--- a/content/test/test_render_view_host.h
+++ b/content/test/test_render_view_host.h
@@ -71,8 +71,8 @@
 
   // RenderWidgetHostView:
   void InitAsChild(gfx::NativeView parent_view) override {}
-  void SetSize(const gfx::Size& size) override {}
-  void SetBounds(const gfx::Rect& rect) override {}
+  void SetSize(const gfx::Size& size) override;
+  void SetBounds(const gfx::Rect& rect) override;
   gfx::NativeView GetNativeView() override;
   gfx::NativeViewAccessible GetNativeViewAccessible() override;
   ui::TextInputClient* GetTextInputClient() override;
@@ -216,6 +216,8 @@
   raw_ptr<ui::Compositor, DanglingUntriaged> compositor_ = nullptr;
 
   input::CursorManager cursor_manager_;
+
+  gfx::Rect bounds_;
 };
 
 // TestRenderWidgetHostViewChildFrame -----------------------------------------
Loading diff…

Original Bug Report

reported by [email protected]

UI spoofing via unclamped EditContext bounds overlaying browser chrome

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: The EditContext API potentially allows malicious web content to position OS-native IME candidate windows outside the visible web content area. Due to a lack of bounds clamping in both the renderer and browser processes, these coordinates can be used to overlay IME windows on browser UI components such as the Omnibox. This enables high-fidelity UI spoofing attacks, particularly for users of CJK Input Method Editors.

Affected files:

  • content/browser/renderer_host/text_input_manager.cc
  • third_party/blink/renderer/core/editing/ime/edit_context.cc
  • content/browser/renderer_host/render_widget_host_view_aura.cc
  • ui/base/ime/win/tsf_text_store.cc
  • ui/base/ime/ash/input_method_ash.cc
  • ui/base/ime/linux/input_method_auralinux.cc
  • ui/base/ime/win/input_method_win_base.cc
  • third_party/blink/renderer/platform/widget/widget_base.cc
  • content/browser/renderer_host/render_widget_host_impl.cc
  • ui/gfx/geometry/mojom/geometry_mojom_traits.h
  • third_party/blink/renderer/core/frame/web_frame_widget_impl.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

A potential security issue has been identified in the implementation of the EditContext API (shipped by default since Chrome 121). The API allows web developers to specify the layout bounds of a text editing area to help the OS position IME (Input Method Editor) candidate windows correctly. However, these bounds are not properly validated or clamped to the visible viewport of the web content. As a result, a malicious site or a compromised renderer can specify coordinates that cause the OS-rendered IME window to appear over privileged browser UI.

Technical Analysis

The vulnerability stems from a lack of bounds checking throughout the coordinate propagation pipeline:

  1. Blink Layer: In third_party/blink/renderer/core/editing/ime/edit_context.cc, the updateControlBounds and updateSelectionBounds methods accept DOMRect objects from JavaScript. The coordinates are only passed through ClampToWithNaNTo0, which handles non-finite values but permits arbitrary negative or out-of-bounds offsets.
  2. IPC Layer: The bounds are sent to the browser process via the ui.mojom.TextInputState struct. The Mojom traits for gfx::Rect (in ui/gfx/geometry/mojom/geometry_mojom_traits.h) only verify that the width and height are non-negative, allowing arbitrary origin coordinates to pass through.
  3. Browser Process: RenderWidgetHostImpl receives the state and caches it in the TextInputManager. In content/browser/renderer_host/text_input_manager.cc, the GetTextControlBounds method transforms the origin to the root coordinate space but preserves the renderer-supplied size without intersecting it with the visible view bounds.
  4. OS Sinks: In content/browser/renderer_host/render_widget_host_view_aura.cc, the ConvertRectToScreen method converts these root-relative coordinates into screen space by adding the window’s screen offset. These final screen coordinates are then passed to OS-native IME APIs, such as Windows Text Services Framework (TSF), ChromeOS Ash, or Linux IBus.

Because the browser does not enforce that these windows remain within the content area, an attacker can position an OS-native window (which can contain attacker-controlled text via the IME composition string) over the Omnibox or permission prompts.

Potential Reproduction Steps

Note: These are suggested steps based on code analysis; a functional proof-of-concept has not yet been executed.

  1. On a system with an active IME (e.g., Japanese MS-IME on Windows), navigate to a page that creates an EditContext:
    const ec = new EditContext();
    const div = document.createElement('div');
    div.editContext = ec;
    document.body.appendChild(div);
    div.focus();
    // Position the bounds to target the omnibox region (e.g., negative Y offset)
    ec.updateSelectionBounds(new DOMRect(0, -80, 200, 20));
    ec.updateControlBounds(new DOMRect(0, -80, 800, 40));
    
  2. Begin typing with the IME active so that a candidate window appears.
  3. Observe whether the IME candidate window appears outside the web content area, potentially overlapping the browser’s toolbar or omnibox.

Impact

This constitutes a UI spoofing vulnerability (Severity: Medium). An attacker can overlay primary browser UI with an OS-native window. This could be used to trick users into entering sensitive information or misinterpreting the security state of the browser.

Suggested Fix

Bounds validation should be implemented in the browser process. Specifically, in content/browser/renderer_host/text_input_manager.cc or the respective RenderWidgetHostView implementations, the bounds retrieved from the renderer should be intersected with the visible bounds of the RenderWidgetHostView before being converted to screen coordinates or passed to OS-native IME APIs.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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