Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Core
DescriptionIncorrect authorization in Core
ComponentCore
Bug ClassLogic Error
Tracker497269030
Fix commitbb53bffbfeb5 (chromium/src) +151/-19
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
PictureInPictureFrameTreeBrowserTest
content/browser/renderer_host/frame_tree_browsertest.cc
modified
PictureInPictureFrameTreeBrowserTest
content/browser/renderer_host/frame_tree_browsertest.cc
modified
BrowserContextGroupSwapFrameTreeBrowserTest
content/browser/renderer_host/frame_tree_browsertest.cc
modified

Files Changed

  • content/browser/bad_message.h
  • content/browser/renderer_host/frame_tree_browsertest.cc
  • content/browser/renderer_host/frame_tree_node.cc
From bb53bffbfeb5f5f6fc898bad7bc68f52a4392de8 Mon Sep 17 00:00:00 2001
From: Tzarial <zork@google.com>
Date: Tue, 14 Jul 2026 08:25:38 -0700
Subject: [PATCH] [agy][content] Gate PiP user activation on origin

FrameTreeNode::NotifyUserActivation() propagates user activation from a
document picture-in-picture window to the root of its opener frame tree.
Unlike other paths, this path did not require the activated frame to be
same-origin with the opener's main frame.

Gate this propagation on IsSameOriginWith(), matching other paths.

Additionally, validate in VerifyCreateNewWindowParams that pip_options
are only allowed when disposition is NEW_PICTURE_IN_PICTURE. This
prevents compromised renderers from forging PiP relationships.

Fixed: 497269030
Test: PictureInPictureFrameTreeBrowserTest.*
Change-Id: I7cb7dc07e8b57335f11597ed7c70878a2fcf573f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8086453
Reviewed-by: Caitlin Fischer <caitlinfischer@google.com>
Commit-Queue: Tzarial <zork@chromium.org>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1661875}
---

diff --git a/content/browser/bad_message.h b/content/browser/bad_message.h
index 0102faa3..c943914 100644
--- a/content/browser/bad_message.h
+++ b/content/browser/bad_message.h
@@ -383,6 +383,7 @@
   RFH_AUTHENTICATOR_PDF_PROCESS_BLOCKED = 355,
   BIBI_BIND_GAMEPAD_MONITOR_BLOCKED_BY_PERMISSIONS_POLICY = 356,
   BIBI_BIND_GAMEPAD_HAPTICS_MANAGER_BLOCKED_BY_PERMISSIONS_POLICY = 357,
+  RFH_CREATE_NEW_WINDOW_INVALID_PIP_OPTIONS = 358,
 
   // Please add new elements here. The naming convention is abbreviated class
   // name (e.g. RenderFrameHost becomes RFH) plus a unique description of the
diff --git a/content/browser/renderer_host/frame_tree_browsertest.cc b/content/browser/renderer_host/frame_tree_browsertest.cc
index 77b119d..146ac7b 100644
--- a/content/browser/renderer_host/frame_tree_browsertest.cc
+++ b/content/browser/renderer_host/frame_tree_browsertest.cc
@@ -13,12 +13,14 @@
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
 #include "content/browser/renderer_host/frame_tree_node.h"
+#include "content/browser/renderer_host/ipc_utils.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_process_host_impl.h"
 #include "content/browser/renderer_host/render_view_host_impl.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/common/content_navigation_policy.h"
+#include "content/common/frame.mojom.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/dedicated_worker_service.h"
 #include "content/public/browser/navigation_handle.h"
@@ -39,6 +41,7 @@
 #include "content/shell/common/shell_switches.h"
 #include "content/test/content_browser_test_utils_internal.h"
 #include "content/test/render_document_feature.h"
+#include "media/base/media_switches.h"
 #include "net/base/features.h"
 #include "net/dns/mock_host_resolver.h"
 #include "net/test/embedded_test_server/controllable_http_response.h"
@@ -1965,6 +1968,118 @@
   EXPECT_FALSE(root->HasTransientUserActivation());
 }
 
+class PictureInPictureFrameTreeBrowserTest
+    : public CrossProcessFrameTreeBrowserTest {
+ public:
+  PictureInPictureFrameTreeBrowserTest() {
+    scoped_feature_list_.InitWithFeatures(
+        {blink::features::kDocumentPictureInPictureUserActivation,
+         media::kDocumentPictureInPictureNavigation},
+        {});
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+// Tests that user activation in a document picture-in-picture window only
+// propagates to the opener window when the activated frame is same-origin with
+// the opener's main frame.
+IN_PROC_BROWSER_TEST_F(PictureInPictureFrameTreeBrowserTest,
+                       UserActivationOpenerPropagationSameOriginOnly) {
+  // Load an a.com page that embeds a cross-origin b.com subframe.
+  GURL main_url(embedded_test_server()->GetURL(
+      "a.com", "/cross_site_iframe_factory.html?a(b)"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  WebContentsImpl* opener_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+  FrameTreeNode* opener_root = opener_contents->GetPrimaryFrameTree().root();
+  ASSERT_EQ(1U, opener_root->child_count());
+  FrameTreeNode* opener_child = opener_root->child_at(0);
+  ASSERT_FALSE(
+      opener_root->current_frame_host()
+          ->GetLastCommittedOrigin()
+          .IsSameOriginWith(
+              opener_child->current_frame_host()->GetLastCommittedOrigin()));
+
+  // Create a picture-in-picture window whose opener is the b.com subframe so
+  // that the picture-in-picture opener resolves to the embedding a.com page.
+  WebContents::CreateParams create_params(
+      opener_contents->GetBrowserContext(),
+      opener_child->current_frame_host()->GetSiteInstance());
+  create_params.picture_in_picture_options =
+      blink::mojom::PictureInPictureWindowOptions();
+  std::unique_ptr<WebContentsImpl> pip_contents =
+      WebContentsImpl::CreateWithOpener(create_params,
+                                        opener_child->current_frame_host());
+  ASSERT_EQ(&opener_contents->GetPrimaryFrameTree(),
+            pip_contents->GetDocumentPictureInPictureOpenerFrameTree());
+
+  // Navigate the picture-in-picture window to a b.com document so that its
+  // main frame is cross-origin with the opener's main frame.
+  GURL cross_origin_url(
+      embedded_test_server()->GetURL("b.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(pip_contents.get(), cross_origin_url));
+  FrameTreeNode* pip_root = pip_contents->GetPrimaryFrameTree().root();
+  ASSERT_FALSE(
+      pip_root->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
+          opener_root->current_frame_host()->GetLastCommittedOrigin()));
+
+  EXPECT_FALSE(opener_root->HasStickyUserActivation());
+  EXPECT_FALSE(opener_root->HasTransientUserActivation());
+
+  // Activating the cross-origin picture-in-picture main frame must not
+  // propagate to the opener.
+  EXPECT_TRUE(pip_root->UpdateUserActivationState(
+      blink::mojom::UserActivationUpdateType::kNotifyActivation,
+      blink::mojom::UserActivationNotificationType::kTest));
+  EXPECT_TRUE(pip_root->HasStickyUserActivation());
+  EXPECT_TRUE(pip_root->HasTransientUserActivation());
+  EXPECT_FALSE(opener_root->HasStickyUserActivation());
+  EXPECT_FALSE(opener_root->HasTransientUserActivation());
+
+  // Navigate the picture-in-picture window to a same-origin a.com document and
+  // verify that activation does propagate to the opener.
+  GURL same_origin_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(pip_contents.get(), same_origin_url));
+  pip_root = pip_contents->GetPrimaryFrameTree().root();
+  ASSERT_TRUE(
+      pip_root->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
+          opener_root->current_frame_host()->GetLastCommittedOrigin()));
+
+  EXPECT_TRUE(pip_root->UpdateUserActivationState(
+      blink::mojom::UserActivationUpdateType::kNotifyActivation,
+      blink::mojom::UserActivationNotificationType::kTest));
+  EXPECT_TRUE(opener_root->HasStickyUserActivation());
+  EXPECT_TRUE(opener_root->HasTransientUserActivation());
+}
+
+IN_PROC_BROWSER_TEST_F(PictureInPictureFrameTreeBrowserTest,
+                       VerifyCreateNewWindowParamsInvalidPipOptions) {
+  EXPECT_TRUE(NavigateToURL(
+      shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
+
+  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+
+  // Create invalid params: NEW_POPUP with pip_options.
+  mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New();
+  params->disposition = WindowOpenDisposition::NEW_POPUP;
+  params->pip_options = blink::mojom::PictureInPictureWindowOptions::New();
+
+  RenderProcessHostWatcher crash_observer(
+      rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
+
+  // Call the validation function. It should return false and trigger bad
+  // message.
+  EXPECT_FALSE(VerifyCreateNewWindowParams(*rfh, *params));
+
+  // The process should be terminated.
+  crash_observer.Wait();
+  EXPECT_FALSE(crash_observer.did_exit_normally());
+}
+
 class BrowserContextGroupSwapFrameTreeBrowserTest : public ContentBrowserTest {
  public:
   BrowserContextGroupSwapFrameTreeBrowserTest()
diff --git a/content/browser/renderer_host/frame_tree_node.cc b/content/browser/renderer_host/frame_tree_node.cc
index 5dba052..c07dca3 100644
--- a/content/browser/renderer_host/frame_tree_node.cc
+++ b/content/browser/renderer_host/frame_tree_node.cc
@@ -796,33 +796,23 @@
   // User Activation V2 requires activating all ancestor frames in addition to
   // the current frame. See
   // https://html.spec.whatwg.org/multipage/interaction.html#tracking-user-activation.
+  // This propagation is spec-mandated to cross origin boundaries (e.g., from
+  // a cross-origin subframe to its parent), so no same-origin check is
+  // performed here.
   for (RenderFrameHostImpl* rfh = current_frame_host(); rfh;
        rfh = rfh->GetParent()) {
     rfh->DidReceiveUserActivation();
     rfh->ActivateUserActivation(notification_type, sticky_only);
   }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/frame_tree_browsertest.cc b/content/browser/renderer_host/frame_tree_browsertest.cc
index 77b119d..146ac7b 100644
--- a/content/browser/renderer_host/frame_tree_browsertest.cc
+++ b/content/browser/renderer_host/frame_tree_browsertest.cc
@@ -13,12 +13,14 @@
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
 #include "content/browser/renderer_host/frame_tree_node.h"
+#include "content/browser/renderer_host/ipc_utils.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_process_host_impl.h"
 #include "content/browser/renderer_host/render_view_host_impl.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/common/content_navigation_policy.h"
+#include "content/common/frame.mojom.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/dedicated_worker_service.h"
 #include "content/public/browser/navigation_handle.h"
@@ -39,6 +41,7 @@
 #include "content/shell/common/shell_switches.h"
 #include "content/test/content_browser_test_utils_internal.h"
 #include "content/test/render_document_feature.h"
+#include "media/base/media_switches.h"
 #include "net/base/features.h"
 #include "net/dns/mock_host_resolver.h"
 #include "net/test/embedded_test_server/controllable_http_response.h"
@@ -1965,6 +1968,118 @@
   EXPECT_FALSE(root->HasTransientUserActivation());
 }
 
+class PictureInPictureFrameTreeBrowserTest
+    : public CrossProcessFrameTreeBrowserTest {
+ public:
+  PictureInPictureFrameTreeBrowserTest() {
+    scoped_feature_list_.InitWithFeatures(
+        {blink::features::kDocumentPictureInPictureUserActivation,
+         media::kDocumentPictureInPictureNavigation},
+        {});
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+// Tests that user activation in a document picture-in-picture window only
+// propagates to the opener window when the activated frame is same-origin with
+// the opener's main frame.
+IN_PROC_BROWSER_TEST_F(PictureInPictureFrameTreeBrowserTest,
+                       UserActivationOpenerPropagationSameOriginOnly) {
+  // Load an a.com page that embeds a cross-origin b.com subframe.
+  GURL main_url(embedded_test_server()->GetURL(
+      "a.com", "/cross_site_iframe_factory.html?a(b)"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  WebContentsImpl* opener_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+  FrameTreeNode* opener_root = opener_contents->GetPrimaryFrameTree().root();
+  ASSERT_EQ(1U, opener_root->child_count());
+  FrameTreeNode* opener_child = opener_root->child_at(0);
+  ASSERT_FALSE(
+      opener_root->current_frame_host()
+          ->GetLastCommittedOrigin()
+          .IsSameOriginWith(
+              opener_child->current_frame_host()->GetLastCommittedOrigin()));
+
+  // Create a picture-in-picture window whose opener is the b.com subframe so
+  // that the picture-in-picture opener resolves to the embedding a.com page.
+  WebContents::CreateParams create_params(
+      opener_contents->GetBrowserContext(),
+      opener_child->current_frame_host()->GetSiteInstance());
+  create_params.picture_in_picture_options =
+      blink::mojom::PictureInPictureWindowOptions();
+  std::unique_ptr<WebContentsImpl> pip_contents =
+      WebContentsImpl::CreateWithOpener(create_params,
+                                        opener_child->current_frame_host());
+  ASSERT_EQ(&opener_contents->GetPrimaryFrameTree(),
+            pip_contents->GetDocumentPictureInPictureOpenerFrameTree());
+
+  // Navigate the picture-in-picture window to a b.com document so that its
+  // main frame is cross-origin with the opener's main frame.
+  GURL cross_origin_url(
+      embedded_test_server()->GetURL("b.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(pip_contents.get(), cross_origin_url));
+  FrameTreeNode* pip_root = pip_contents->GetPrimaryFrameTree().root();
+  ASSERT_FALSE(
+      pip_root->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
+          opener_root->current_frame_host()->GetLastCommittedOrigin()));
+
+  EXPECT_FALSE(opener_root->HasStickyUserActivation());
+  EXPECT_FALSE(opener_root->HasTransientUserActivation());
+
+  // Activating the cross-origin picture-in-picture main frame must not
+  // propagate to the opener.
+  EXPECT_TRUE(pip_root->UpdateUserActivationState(
+      blink::mojom::UserActivationUpdateType::kNotifyActivation,
+      blink::mojom::UserActivationNotificationType::kTest));
+  EXPECT_TRUE(pip_root->HasStickyUserActivation());
+  EXPECT_TRUE(pip_root->HasTransientUserActivation());
+  EXPECT_FALSE(opener_root->HasStickyUserActivation());
+  EXPECT_FALSE(opener_root->HasTransientUserActivation());
+
+  // Navigate the picture-in-picture window to a same-origin a.com document and
+  // verify that activation does propagate to the opener.
+  GURL same_origin_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(pip_contents.get(), same_origin_url));
+  pip_root = pip_contents->GetPrimaryFrameTree().root();
+  ASSERT_TRUE(
+      pip_root->current_frame_host()->GetLastCommittedOrigin().IsSameOriginWith(
+          opener_root->current_frame_host()->GetLastCommittedOrigin()));
+
+  EXPECT_TRUE(pip_root->UpdateUserActivationState(
+      blink::mojom::UserActivationUpdateType::kNotifyActivation,
+      blink::mojom::UserActivationNotificationType::kTest));
+  EXPECT_TRUE(opener_root->HasStickyUserActivation());
+  EXPECT_TRUE(opener_root->HasTransientUserActivation());
+}
+
+IN_PROC_BROWSER_TEST_F(PictureInPictureFrameTreeBrowserTest,
+                       VerifyCreateNewWindowParamsInvalidPipOptions) {
+  EXPECT_TRUE(NavigateToURL(
+      shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
+
+  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+
+  // Create invalid params: NEW_POPUP with pip_options.
+  mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New();
+  params->disposition = WindowOpenDisposition::NEW_POPUP;
+  params->pip_options = blink::mojom::PictureInPictureWindowOptions::New();
+
+  RenderProcessHostWatcher crash_observer(
+      rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
+
+  // Call the validation function. It should return false and trigger bad
+  // message.
+  EXPECT_FALSE(VerifyCreateNewWindowParams(*rfh, *params));
+
+  // The process should be terminated.
+  crash_observer.Wait();
+  EXPECT_FALSE(crash_observer.did_exit_normally());
+}
+
 class BrowserContextGroupSwapFrameTreeBrowserTest : public ContentBrowserTest {
  public:
   BrowserContextGroupSwapFrameTreeBrowserTest()
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.