Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Navigation
DescriptionInappropriate implementation in Navigation
ComponentNavigation
Bug ClassLogic Error
Tracker514519203
Fix commite7347570385c (chromium/src) +115/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
FocusChangedWatcher
content/browser/fenced_frame/fenced_frame_browsertest.cc
modified
FencedFrameMPArchBrowserTestWithEnforceFocusDisabled
content/browser/fenced_frame/fenced_frame_browsertest.cc
modified
FencedFrameMPArchBrowserTestWithEnforceFocusDisabled
content/browser/fenced_frame/fenced_frame_browsertest.cc
modified

Files Changed

  • content/browser/fenced_frame/fenced_frame_browsertest.cc
  • content/browser/web_contents/web_contents_impl.cc
From e7347570385c312e73a6d98abd876c5dd5e716ff Mon Sep 17 00:00:00 2001
From: Arthur Sonzogni <[email protected]>
Date: Mon, 15 Jun 2026 08:26:02 -0700
Subject: [PATCH] fix: Ignore FocusedElementChanged from unfocused frames

Prevent unfocused frames (such as fenced frames without user activation)
from updating the browser's focus/IME state on the root view.

Previously, WebContentsImpl::OnFocusedElementChangedInFrame processed
FocusedElementChanged IPCs from any frame regardless of its focus
status. This allowed unfocused fenced frames to spoof focus and disrupt
the embedder's IME/virtual keyboard state.

This CL adds a check to WebContentsImpl::OnFocusedElementChangedInFrame
to ensure the notifying frame is the currently focused frame
(GetFocusedFrame()). If not, the notification is ignored.

A regression test is added to fenced_frame_browsertest.cc.

TAG=agy
CONV=fabada71-c488-4f98-827b-4994438e0d90

Bug: 514519203
Change-Id: Ie0c4965f7706ccb9a281899135727f035eda41b8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7869565
Reviewed-by: Liam Brady <[email protected]>
Commit-Queue: Arthur Sonzogni <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1646831}
---

diff --git a/content/browser/fenced_frame/fenced_frame_browsertest.cc b/content/browser/fenced_frame/fenced_frame_browsertest.cc
index 56ebed0..b1dc653 100644
--- a/content/browser/fenced_frame/fenced_frame_browsertest.cc
+++ b/content/browser/fenced_frame/fenced_frame_browsertest.cc
@@ -16,6 +16,7 @@
 #include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
+#include "base/test/test_future.h"
 #include "base/test/values_test_util.h"
 #include "base/time/time.h"
 #include "build/build_config.h"
@@ -33,10 +34,12 @@
 #include "content/browser/renderer_host/frame_tree_node.h"
 #include "content/browser/renderer_host/navigation_entry_restore_context_impl.h"
 #include "content/browser/renderer_host/navigation_request.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/browser/renderer_host/render_frame_proxy_host.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/common/features.h"
 #include "content/public/browser/browser_context.h"
+#include "content/public/browser/focused_node_details.h"
 #include "content/public/browser/frame_type.h"
 #include "content/public/browser/navigation_handle.h"
 #include "content/public/common/content_features.h"
@@ -1146,6 +1149,110 @@
   EXPECT_EQ(web_contents()->GetFocusedFrame(), fenced_frame_rfh.get());
 }
 
+class FocusChangedWatcher : public WebContentsObserver {
+ public:
+  explicit FocusChangedWatcher(WebContents* web_contents)
+      : WebContentsObserver(web_contents) {}
+
+  void OnFocusChangedInPage(const FocusedNodeDetails& details) override {
+    future_.SetValue(details);
+  }
+
+  const FocusedNodeDetails& Wait() { return future_.Get(); }
+  bool observed() const { return future_.IsReady(); }
+
+ private:
+  base::test::TestFuture<FocusedNodeDetails> future_;
+};
+
+class FencedFrameMPArchBrowserTestWithEnforceFocusDisabled
+    : public FencedFrameMPArchBrowserTest {
+ public:
+  FencedFrameMPArchBrowserTestWithEnforceFocusDisabled() {
+    feature_list_.InitAndDisableFeature(features::kFencedFramesEnforceFocus);
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+// Regression test for crbug.com/514519203.
+// Verify that an unfocused fenced frame cannot trigger focused element changed
+// notifications on the root view.
+IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTestWithEnforceFocusDisabled,
+                       FencedFrameFocusedElementChangedWithoutFocus) {
+  ASSERT_TRUE(https_server()->Start());
+  const GURL url = https_server()->GetURL("c.test", "/title1.html");
+  ASSERT_TRUE(NavigateToURL(shell(), url));
+
+  // 1. Focus primary main frame input.
+  {
+    FocusChangedWatcher watcher(web_contents());
+    ASSERT_TRUE(ExecJs(primary_main_frame_host(),
+                       "const input = document.createElement('input');"
+                       "input.id = 'primary_input';"
+                       "document.body.appendChild(input);"
+                       "input.focus();"));
+    const FocusedNodeDetails& details = watcher.Wait();
+    EXPECT_TRUE(details.is_editable_node);
+  }
+
+  // 2. Create fenced frame and add two inputs.
+  const GURL fenced_frame_url =
+      https_server()->GetURL("c.test", "/fenced_frames/title1.html");
+  RenderFrameHostImplWrapper fenced_frame_rfh(
+      fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(),
+                                                   fenced_frame_url));
+  ASSERT_TRUE(ExecJs(fenced_frame_rfh.get(),
+                     "const input1 = document.createElement('input');"
+                     "input1.id = 'fenced_input1';"
+                     "document.body.appendChild(input1);"
+                     "const input2 = document.createElement('input');"
+                     "input2.id = 'fenced_input2';"
+                     "document.body.appendChild(input2);"));
+
+  // 3. Focus fenced_input1 WITH user gesture.
+  {
+    FocusChangedWatcher watcher(web_contents());
+    ASSERT_TRUE(ExecJs(fenced_frame_rfh.get(),
+                       "document.getElementById('fenced_input1').focus();"));
+    const FocusedNodeDetails& details = watcher.Wait();
+    EXPECT_TRUE(details.is_editable_node);
+  }
+
+  // 4. Focus primary main frame input WITH user gesture.
+  {
+    FocusChangedWatcher watcher(web_contents());
+    ASSERT_TRUE(ExecJs(primary_main_frame_host(),
+                       "const input = document.createElement('input');"
+                       "input.id = 'primary_input';"
+                       "document.body.appendChild(input);"
+                       "input.focus();"));
+    const FocusedNodeDetails& details = watcher.Wait();
+    EXPECT_TRUE(details.is_editable_node);
+  }
+
+  // Clear user activation on the fenced frame to ensure it doesn't have
+  // transient user activation from step 3.
+  static_cast<RenderFrameHostImpl*>(fenced_frame_rfh.get())
+      ->ClearUserActivation();
+
+  // 5. Try to focus fenced_input2 WITHOUT user gesture.
+  // Fenced frame is NOT focused now.
+  FocusChangedWatcher final_watcher(web_contents());
+  ASSERT_TRUE(ExecJs(fenced_frame_rfh.get(),
+                     "document.getElementById('fenced_input2').focus();",
+                     EXECUTE_SCRIPT_NO_USER_GESTURE));
+
+  // Force a roundtrip to ensure any pending IPCs are processed.
+  EXPECT_EQ(true, EvalJs(fenced_frame_rfh.get(), "true"));
+
+  // If the bug is present, the unfocused fenced frame can still trigger
+  // FocusedElementChanged, which would notify our observer.
+  // We expect it to be ignored (after fix).
+  EXPECT_FALSE(final_watcher.observed());
+}
+
 // Test that the initial navigation in a fenced frame, which navigates from the
 // initial empty document, is not classified as a client redirect.
 IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest,
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index 349e8f4..b3468b9 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -10700,6 +10700,14 @@
   OPTIONAL_TRACE_EVENT1("content",
                         "WebContentsImpl::OnFocusedElementChangedInFrame",
                         "render_frame_host", frame);
+  // Only apply focus updates from the currently focused frame. We ignore
+  // updates from unfocused frames (instead of treating them as bad messages)
+  // because document-local focus changes are allowed, and focus transitions
+  // can race asynchronously. Focus theft is already handled and blocked (with
+  // bad messages) in `RenderFrameHostImpl::VerifyFencedFrameFocusChange`.
+  if (frame != GetFocusedFrame()) {
+    return;
+  }
   RenderWidgetHostViewBase* root_view =
       static_cast<RenderWidgetHostViewBase*>(GetRenderWidgetHostView());
   if (!root_view || !frame->GetView()) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/fenced_frame/fenced_frame_browsertest.cc b/content/browser/fenced_frame/fenced_frame_browsertest.cc
index 56ebed0..b1dc653 100644
--- a/content/browser/fenced_frame/fenced_frame_browsertest.cc
+++ b/content/browser/fenced_frame/fenced_frame_browsertest.cc
@@ -16,6 +16,7 @@
 #include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
+#include "base/test/test_future.h"
 #include "base/test/values_test_util.h"
 #include "base/time/time.h"
 #include "build/build_config.h"
@@ -33,10 +34,12 @@
 #include "content/browser/renderer_host/frame_tree_node.h"
 #include "content/browser/renderer_host/navigation_entry_restore_context_impl.h"
 #include "content/browser/renderer_host/navigation_request.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/browser/renderer_host/render_frame_proxy_host.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/common/features.h"
 #include "content/public/browser/browser_context.h"
+#include "content/public/browser/focused_node_details.h"
 #include "content/public/browser/frame_type.h"
 #include "content/public/browser/navigation_handle.h"
 #include "content/public/common/content_features.h"
@@ -1146,6 +1149,110 @@
   EXPECT_EQ(web_contents()->GetFocusedFrame(), fenced_frame_rfh.get());
 }
 
+class FocusChangedWatcher : public WebContentsObserver {
+ public:
+  explicit FocusChangedWatcher(WebContents* web_contents)
+      : WebContentsObserver(web_contents) {}
+
+  void OnFocusChangedInPage(const FocusedNodeDetails& details) override {
+    future_.SetValue(details);
+  }
+
+  const FocusedNodeDetails& Wait() { return future_.Get(); }
+  bool observed() const { return future_.IsReady(); }
+
+ private:
+  base::test::TestFuture<FocusedNodeDetails> future_;
+};
+
+class FencedFrameMPArchBrowserTestWithEnforceFocusDisabled
+    : public FencedFrameMPArchBrowserTest {
+ public:
+  FencedFrameMPArchBrowserTestWithEnforceFocusDisabled() {
+    feature_list_.InitAndDisableFeature(features::kFencedFramesEnforceFocus);
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+// Regression test for crbug.com/514519203.
+// Verify that an unfocused fenced frame cannot trigger focused element changed
+// notifications on the root view.
+IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTestWithEnforceFocusDisabled,
+                       FencedFrameFocusedElementChangedWithoutFocus) {
+  ASSERT_TRUE(https_server()->Start());
+  const GURL url = https_server()->GetURL("c.test", "/title1.html");
+  ASSERT_TRUE(NavigateToURL(shell(), url));
+
+  // 1. Focus primary main frame input.
+  {
+    FocusChangedWatcher watcher(web_contents());
+    ASSERT_TRUE(ExecJs(primary_main_frame_host(),
+                       "const input = document.createElement('input');"
+                       "input.id = 'primary_input';"
+                       "document.body.appendChild(input);"
+                       "input.focus();"));
+    const FocusedNodeDetails& details = watcher.Wait();
+    EXPECT_TRUE(details.is_editable_node);
+  }
+
+  // 2. Create fenced frame and add two inputs.
+  const GURL fenced_frame_url =
+      https_server()->GetURL("c.test", "/fenced_frames/title1.html");
+  RenderFrameHostImplWrapper fenced_frame_rfh(
+      fenced_frame_test_helper().CreateFencedFrame(primary_main_frame_host(),
+                                                   fenced_frame_url));
+  ASSERT_TRUE(ExecJs(fenced_frame_rfh.get(),
+                     "const input1 = document.createElement('input');"
+                     "input1.id = 'fenced_input1';"
+                     "document.body.appendChild(input1);"
+                     "const input2 = document.createElement('input');"
+                     "input2.id = 'fenced_input2';"
+                     "document.body.appendChild(input2);"));
+
+  // 3. Focus fenced_input1 WITH user gesture.
+  {
+    FocusChangedWatcher watcher(web_contents());
+    ASSERT_TRUE(ExecJs(fenced_frame_rfh.get(),
+                       "document.getElementById('fenced_input1').focus();"));
+    const FocusedNodeDetails& details = watcher.Wait();
+    EXPECT_TRUE(details.is_editable_node);
+  }
+
+  // 4. Focus primary main frame input WITH user gesture.
+  {
+    FocusChangedWatcher watcher(web_contents());
+    ASSERT_TRUE(ExecJs(primary_main_frame_host(),
+                       "const input = document.createElement('input');"
+                       "input.id = 'primary_input';"
+                       "document.body.appendChild(input);"
+                       "input.focus();"));
+    const FocusedNodeDetails& details = watcher.Wait();
+    EXPECT_TRUE(details.is_editable_node);
+  }
+
+  // Clear user activation on the fenced frame to ensure it doesn't have
+  // transient user activation from step 3.
+  static_cast<RenderFrameHostImpl*>(fenced_frame_rfh.get())
+      ->ClearUserActivation();
+
+  // 5. Try to focus fenced_input2 WITHOUT user gesture.
+  // Fenced frame is NOT focused now.
+  FocusChangedWatcher final_watcher(web_contents());
+  ASSERT_TRUE(ExecJs(fenced_frame_rfh.get(),
+                     "document.getElementById('fenced_input2').focus();",
+                     EXECUTE_SCRIPT_NO_USER_GESTURE));
+
+  // Force a roundtrip to ensure any pending IPCs are processed.
+  EXPECT_EQ(true, EvalJs(fenced_frame_rfh.get(), "true"));
+
+  // If the bug is present, the unfocused fenced frame can still trigger
+  // FocusedElementChanged, which would notify our observer.
+  // We expect it to be ignored (after fix).
+  EXPECT_FALSE(final_watcher.observed());
+}
+
 // Test that the initial navigation in a fenced frame, which navigates from the
 // initial empty document, is not classified as a client redirect.
 IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest,
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.