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 <[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.
References
On This Page