Chrome · Navigation
CVE-2026-17945
Logic Error in Navigation
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
FocusChangedWatchercontent/browser/fenced_frame/fenced_frame_browsertest.cc |
modified | |
FencedFrameMPArchBrowserTestWithEnforceFocusDisabledcontent/browser/fenced_frame/fenced_frame_browsertest.cc |
modified | |
FencedFrameMPArchBrowserTestWithEnforceFocusDisabledcontent/browser/fenced_frame/fenced_frame_browsertest.cc |
modified |
Files Changed
content/browser/fenced_frame/fenced_frame_browsertest.cccontent/browser/web_contents/web_contents_impl.cc
Patch
From e7347570385c312e73a6d98abd876c5dd5e716ff Mon Sep 17 00:00:00 2001
From: Arthur Sonzogni <arthursonzogni@chromium.org>
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 <lbrady@google.com>
Commit-Queue: Arthur Sonzogni <arthursonzogni@chromium.org>
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.
References
On This Page