Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in PictureInPicture
DescriptionIncorrect authorization in PictureInPicture
ComponentPictureInPicture
Sub-componentAutoPiP
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker553268567
Fix commit7e945a3a36ac (chromium/src) +112/-9
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-08-27 (per vendor advisory)
Fix landed2026-08-31 (public git)
Disclosed2026-09-22
Patch gap22 days — fix public in source before the advisory

Background

Auto Picture-in-Picture
a feature that lets an eligible document automatically enter a floating video overlay (Auto-PiP) when the user switches away from the tab, gated by permission and eligibility checks.
Opaque origin
a security origin with no meaningful scheme/host (serialized as "null") assigned to documents committed under a CSP sandbox directive lacking allow-same-origin.
`AutoPictureInPictureTabHelper`
the WebContents-scoped browser-side helper that decides eligibility and drives Auto-PiP entry for the primary main frame.
`PrimaryMainFrameHasOpaqueOrigin()`
a helper that reports whether the primary main frame committed with an opaque security origin, distinguishing sandboxed documents from same-origin ones.

Root Cause Analysis

The eligibility and permission logic in AutoPictureInPictureTabHelper::IsEligibleForAutoPictureInPicture keyed its decisions on the URL returned by WebContents::GetLastCommittedURL() without checking whether the primary main frame actually committed with an opaque origin. A CSP-sandboxed document (served without allow-same-origin) commits with an opaque origin yet still reports the outer document’s last-committed URL, so the sandboxed frame was treated as if it carried the URL’s origin. This violated the invariant that a document should only exercise permissions belonging to its own security origin, letting a sandboxed document enter Auto-PiP and inherit permissions granted to the outer origin.

The fix adds an explicit PrimaryMainFrameHasOpaqueOrigin() check so that documents with an opaque origin are excluded from eligibility. Because opaque origins cannot legitimately hold origin-scoped permissions, denying them Auto-PiP restores the origin-authorization invariant.

Key insight
The core mistake was authorizing Auto-PiP from the last-committed URL alone, ignoring that a sandboxed document commits with an opaque origin distinct from that URL; the fix adds PrimaryMainFrameHasOpaqueOrigin() so eligibility is denied whenever the frame’s real origin is opaque.

Attack Path

  1. Serve a sandboxed document An attacker hosts (or is embedded on) a page delivered with a CSP sandbox directive that omits allow-same-origin, causing the primary main frame to commit with an opaque origin while still resolving to the outer URL.
  2. Trigger Auto-PiP eligibility The document plays media and registers for Auto-PiP; IsEligibleForAutoPictureInPicture evaluates permission using WebContents::GetLastCommittedURL() and, lacking an opaque-origin check, treats the sandboxed frame as the outer origin.
  3. Inherit outer-origin permissions The sandboxed document enters Auto-PiP under permissions that belong to the outer origin, bypassing the intended origin authorization boundary.
  4. Abuse the inherited capability (inferred) With Auto-PiP granted under a stronger origin’s authorization, the sandboxed content could present overlay UI or exercise the feature in a context the sandbox was meant to prevent.

Impact Assessment

An attacker-controlled sandboxed document gains incorrect authorization to enter Auto Picture-in-Picture in the browser, inheriting Auto-PiP permission scoped to the outer origin rather than its own opaque origin, all within the renderer/browser Picture-in-Picture flow. The precondition is that the document commit with an opaque origin (a CSP sandbox directive without allow-same-origin) while media and Auto-PiP registration proceed. The advisory classifies this as a LogicError / incorrect-authorization issue and states no exploited-in-the-wild status; any escalation beyond wrongful Auto-PiP entry is (inferred).

Changed Functions

FunctionChangeNotes
if
chrome/android/java/src/org/chromium/chrome/browser/media/AutoPictureInPicturePermissionController.java
modified
if
chrome/android/junit/src/org/chromium/chrome/browser/media/AutoPictureInPicturePermissionControllerTest.java
modified

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/media/AutoPictureInPicturePermissionController.java
  • chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html.mock-http-headers
  • chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.cc
  • chrome/android/junit/src/org/chromium/chrome/browser/media/AutoPictureInPicturePermissionControllerTest.java
  • chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.h
  • chrome/browser/picture_in_picture/test/android/java/src/org/chromium/chrome/browser/picture_in_picture/AutoPictureInPictureTabHelperTest.java
  • chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html

Audit Directions

  • URL-vs-origin authorization
    Audit permission and eligibility checks that key off GetLastCommittedURL() or similar URL getters instead of the frame’s actual security origin, and add opaque-origin guards.
  • Sandboxed-frame inheritance
    Review features reachable from documents committed under CSP sandbox without allow-same-origin, checking whether they can inherit origin-scoped permissions.
  • Prompt/controller lifecycle
    Look for controllers or observers registered before their UI is confirmed shown, where an unshown component is left registered as a valid owner.
From 7e945a3a36ac49e03b1da1764ec4721a7136f9a5 Mon Sep 17 00:00:00 2001
From: Phil Yan <philyan@chromium.org>
Date: Mon, 31 Aug 2026 13:18:04 -0700
Subject: [PATCH] [AutoPiP] Block auto-pip for opaque origins

Documents served with CSP sandbox directives without 'allow-same-origin'
commit with an opaque origin (window.origin === "null"). Previously,
Auto-PiP permission and eligibility checks relied on the URL from
WebContents::GetLastCommittedURL() without checking whether the primary
main frame has an opaque security origin. This allowed sandboxed
documents to enter Auto-PiP and inherit permissions belonging to the
outer origin.

This CL ensures sandboxed documents cannot enter Auto-PiP:
- AutoPictureInPictureTabHelper::IsEligibleForAutoPictureInPicture
  verifies that the primary main frame does not have an opaque origin
  via PrimaryMainFrameHasOpaqueOrigin().
- AutoPictureInPicturePermissionController in Java registers the
  permission controller conditionally only when prompt display
  succeeds, eliminating unshown controller rollback logic.

TAG=agy
CONV=18d20bd4-5827-471b-9457-125cd32dda4f

Bug: 553268567
Test: chrome_junit_tests (AutoPictureInPicturePermissionControllerTest)
Test: run_chrome_public_test_apk (AutoPictureInPictureTabHelperTest)
Change-Id: If1dbac4c3eecb9e27ba98040622955a70460c18b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8311508
Commit-Queue: Phil Yan <philyan@chromium.org>
Reviewed-by: Benjamin Keen <bkeen@google.com>
Cr-Commit-Position: refs/heads/main@{#1689195}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/AutoPictureInPicturePermissionController.java b/chrome/android/java/src/org/chromium/chrome/browser/media/AutoPictureInPicturePermissionController.java
index d8b891f64..477515c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/media/AutoPictureInPicturePermissionController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/AutoPictureInPicturePermissionController.java
@@ -89,8 +89,8 @@
         AutoPictureInPictureTabHelper helper =
                 assertNonNull(AutoPictureInPictureTabHelper.fromWebContents(webContents));
 
-        // Create the controller and register it with the helper. This prevents "fire and forget"
-        // by giving the controller a clear owner (the helper attached to the WebContents).
+        // Create the controller and register it with the helper only if it is successfully shown.
+        // This prevents "fire and forget" while ensuring an unshown controller is never registered.
         AutoPictureInPicturePermissionController controller =
                 new AutoPictureInPicturePermissionController(webContents, closePipCallback);
 
@@ -104,9 +104,10 @@
                         pipActivity.revertToRequestedBounds();
                     }
                 };
-        helper.setPermissionController(controller);
 
-        controller.show(activity);
+        if (controller.show(activity)) {
+            helper.setPermissionController(controller);
+        }
     }
 
     /**
@@ -116,7 +117,7 @@
      * @return True if a prompt is needed, false otherwise.
      */
     public static boolean isPermissionPromptNeeded(WebContents webContents) {
-        if (!isAutoPictureInPictureInUse(webContents)) {
+        if (webContents == null || !isAutoPictureInPictureInUse(webContents)) {
             return false;
         }
 
@@ -170,9 +171,9 @@
         mClosePipCallback = closePipCallback;
     }
 
-    private void show(Activity activity) {
+    private boolean show(Activity activity) {
         if (mWebContents.isDestroyed() || activity.isFinishing() || activity.isDestroyed()) {
-            return;
+            return false;
         }
 
         mActivityWeakRef = new WeakReference<>(activity);
@@ -180,7 +181,7 @@
         ViewGroup rootView = activity.findViewById(android.R.id.content);
         if (rootView == null) {
             Log.w(TAG, "Could not find root view to attach Auto-PiP prompt.");
-            return;
+            return false;
         }
 
         // Add the privacy mask first, so it's behind the prompt. Use the activity context directly
@@ -193,6 +194,7 @@
 
         GlobalNightModeStateProviderHolder.getInstance().addObserver(this);
         updateViews();
+        return true;
     }
 
     @Override
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/media/AutoPictureInPicturePermissionControllerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/media/AutoPictureInPicturePermissionControllerTest.java
index 057b96da..b8993e5 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/media/AutoPictureInPicturePermissionControllerTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/media/AutoPictureInPicturePermissionControllerTest.java
@@ -100,7 +100,7 @@
 
     @After
     public void tearDown() {
-        if (mActivityController != null) {
+        if (mActivityController != null && mActivity != null && !mActivity.isDestroyed()) {
             mActivityController.pause().stop().destroy();
         }
     }
@@ -172,6 +172,30 @@
     }
 
     @Test
+    public void testShowPrompt_WhenActivityFinishing_DoesNotRegisterController() {
+        when(mNativeMock.getPermissionStatus(mWebContents)).thenReturn(ContentSetting.ASK);
+        mActivity.finish();
+
+        AutoPictureInPicturePermissionController.showPromptIfNeeded(
+                mActivity, mTab, NO_OP_CALLBACK);
+
+        ViewGroup rootView = mActivity.findViewById(android.R.id.content);
+        Assert.assertEquals(0, rootView.getChildCount());
+        Assert.assertNull(mTabHelper.getPermissionController());
+    }
+
+    @Test
+    public void testShowPrompt_WhenActivityDestroyed_DoesNotRegisterController() {
+        when(mNativeMock.getPermissionStatus(mWebContents)).thenReturn(ContentSetting.ASK);
+        mActivityController.pause().stop().destroy();
+
+        AutoPictureInPicturePermissionController.showPromptIfNeeded(
+                mActivity, mTab, NO_OP_CALLBACK);
+
+        Assert.assertNull(mTabHelper.getPermissionController());
+    }
+
+    @Test
     public void testShowPrompt_AllowOnce_PreventsSubsequentPrompt() {
         // Show prompt and simulate "Allow Once" click.
         when(mNativeMock.getPermissionStatus(mWebContents)).thenReturn(ContentSetting.ASK);
diff --git a/chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.cc b/chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.cc
index 1462993..a79df1d 100644
--- a/chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.cc
+++ b/chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.cc
@@ -27,6 +27,7 @@
 #include "components/permissions/permission_decision_auto_blocker.h"
 #include "content/public/browser/media_session.h"
 #include "content/public/browser/media_session_service.h"
+#include "content/public/browser/render_frame_host.h"
 #include "media/base/media_switches.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "third_party/blink/public/common/features.h"
@@ -123,6 +124,14 @@
   return has_ever_registered_for_auto_picture_in_picture_;
 }
 
+bool AutoPictureInPictureTabHelper::PrimaryMainFrameHasOpaqueOrigin() const {
+  if (!web_contents()) {
+    return false;
+  }
+  const content::RenderFrameHost* rfh = web_contents()->GetPrimaryMainFrame();
+  return rfh && rfh->GetLastCommittedOrigin().opaque();
+}
+
 void AutoPictureInPictureTabHelper::PrimaryPageChanged(content::Page& page) {
   has_ever_registered_for_auto_picture_in_picture_ = false;
   // On navigation, forget any 'allow once' state.
@@ -658,6 +667,10 @@
     return false;
   }
 
+  if (PrimaryMainFrameHasOpaqueOrigin()) {
+    return false;
+  }
+
   // Only https:// or file:// may autopip.
   const GURL url = web_contents()->GetLastCommittedURL();
   if (!url.SchemeIs(url::kHttpsScheme) && !url.SchemeIsFile()) {
diff --git a/chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.h b/chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.h
index 5a7868dd7..496945d 100644
--- a/chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.h
+++ b/chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.h
@@ -245,6 +245,9 @@
 
   bool IsEligibleForAutoPictureInPicture(bool should_record_blocking_metrics);
 
+  // Returns true if the primary main frame has an opaque security origin.
+  bool PrimaryMainFrameHasOpaqueOrigin() const;
+
   // Returns true if the tab:
   //   * Has audio focus
   //   * Is playing unmuted playback
diff --git a/chrome/browser/picture_in_picture/test/android/java/src/org/chromium/chrome/browser/picture_in_picture/AutoPictureInPictureTabHelperTest.java b/chrome/browser/picture_in_picture/test/android/java/src/org/chromium/chrome/browser/picture_in_picture/AutoPictureInPictureTabHelperTest.java
index 911c51b..13b3df56 100644
--- a/chrome/browser/picture_in_picture/test/android/java/src/org/chromium/chrome/browser/picture_in_picture/AutoPictureInPictureTabHelperTest.java
+++ b/chrome/browser/picture_in_picture/test/android/java/src/org/chromium/chrome/browser/picture_in_picture/AutoPictureInPictureTabHelperTest.java
@@ -92,6 +92,8 @@
     private static final String PIP_BUTTON_ID = "pip";
     private static final String AUTO_PIP_VIDEO_PAGE =
             "/chrome/test/data/media/picture-in-picture/autopip-video.html";
+    private static final String AUTO_PIP_VIDEO_SANDBOXED_PAGE =
+            "/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html";
     private static final String AUTO_PIP_NOT_REGISTERED_PAGE =
             "/chrome/test/data/media/picture-in-picture/autopip-no-register.html";
     private static final String VIDEO_CONFERENCING_PAGE =
@@ -294,6 +296,29 @@
 
     @Test
     @MediumTest
+    public void testDoesNotAutopipWithOpaqueOrigin() throws TimeoutException {
+        WebContents webContents = loadUrlAndInitializeForTest(AUTO_PIP_VIDEO_SANDBOXED_PAGE);
+        assertTrue(
+                "Page should have registered for auto-pip.",
+                AutoPictureInPictureTabHelperTestUtils.hasAutoPictureInPictureBeenRegistered(
+                        webContents));
+
+        // Create a new tab in the background to switch to later.
+        Tab originalTab = mPage.getTab();
+        Tab newTab = createNewTabInBackground(originalTab);
+
+        fulfillVideoPlaybackConditions(webContents);
+
+        // Switch away from the tab.
+        switchToTab(newTab);
+
+        // Since the site has an opaque origin, it should not enter auto-PiP.
+        AutoPictureInPictureTabHelperTestUtils.waitForAutoPictureInPictureState(
+                webContents, false, "Should not enter auto-PiP with opaque origin.");
+    }
+
+    @Test
+    @MediumTest
     public void testDoesNotAutopipWithoutPlayback() {
         WebContents webContents = loadUrlAndInitializeForTest(AUTO_PIP_VIDEO_PAGE);
         assertTrue(
diff --git a/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html b/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html
new file mode 100644
index 0000000..600ce9d5
--- /dev/null
+++ b/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8">
+<title>Autopip Video Sandboxed</title>
+</head>
+<body>
+  <video id="video" controls loop>
+    <source src="../bigbuck.webm">
+  </video>
+  <button id="pip" onclick="openPip()">Enter Picture-in-Picture</button>
+</body>
+<script>
+const video = document.getElementById('video');
+
+function playVideo() {
+  navigator.mediaSession.playbackState = "playing";
+  video.play();
+}
+
+function pauseVideo() {
+  video.pause();
+  navigator.mediaSession.playbackState = "paused";
+}
+
+function muteVideo() {
+  video.muted = true;
+}
+
+function openPip(details) {
+  video.requestPictureInPicture();
+}
+
+navigator.mediaSession.setActionHandler('enterpictureinpicture', openPip);
+</script>
diff --git a/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html.mock-http-headers b/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html.mock-http-headers
new file mode 100644
index 0000000..82539c23
--- /dev/null
+++ b/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html.mock-http-headers
@@ -0,0 +1,2 @@
+HTTP/1.1 200 OK
+Content-Security-Policy: sandbox allow-scripts
\ No newline at end of file
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/picture_in_picture/test/android/java/src/org/chromium/chrome/browser/picture_in_picture/AutoPictureInPictureTabHelperTest.java b/chrome/browser/picture_in_picture/test/android/java/src/org/chromium/chrome/browser/picture_in_picture/AutoPictureInPictureTabHelperTest.java
index 911c51b..13b3df56 100644
--- a/chrome/browser/picture_in_picture/test/android/java/src/org/chromium/chrome/browser/picture_in_picture/AutoPictureInPictureTabHelperTest.java
+++ b/chrome/browser/picture_in_picture/test/android/java/src/org/chromium/chrome/browser/picture_in_picture/AutoPictureInPictureTabHelperTest.java
@@ -92,6 +92,8 @@
     private static final String PIP_BUTTON_ID = "pip";
     private static final String AUTO_PIP_VIDEO_PAGE =
             "/chrome/test/data/media/picture-in-picture/autopip-video.html";
+    private static final String AUTO_PIP_VIDEO_SANDBOXED_PAGE =
+            "/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html";
     private static final String AUTO_PIP_NOT_REGISTERED_PAGE =
             "/chrome/test/data/media/picture-in-picture/autopip-no-register.html";
     private static final String VIDEO_CONFERENCING_PAGE =
@@ -294,6 +296,29 @@
 
     @Test
     @MediumTest
+    public void testDoesNotAutopipWithOpaqueOrigin() throws TimeoutException {
+        WebContents webContents = loadUrlAndInitializeForTest(AUTO_PIP_VIDEO_SANDBOXED_PAGE);
+        assertTrue(
+                "Page should have registered for auto-pip.",
+                AutoPictureInPictureTabHelperTestUtils.hasAutoPictureInPictureBeenRegistered(
+                        webContents));
+
+        // Create a new tab in the background to switch to later.
+        Tab originalTab = mPage.getTab();
+        Tab newTab = createNewTabInBackground(originalTab);
+
+        fulfillVideoPlaybackConditions(webContents);
+
+        // Switch away from the tab.
+        switchToTab(newTab);
+
+        // Since the site has an opaque origin, it should not enter auto-PiP.
+        AutoPictureInPictureTabHelperTestUtils.waitForAutoPictureInPictureState(
+                webContents, false, "Should not enter auto-PiP with opaque origin.");
+    }
+
+    @Test
+    @MediumTest
     public void testDoesNotAutopipWithoutPlayback() {
         WebContents webContents = loadUrlAndInitializeForTest(AUTO_PIP_VIDEO_PAGE);
         assertTrue(
diff --git a/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html b/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html
new file mode 100644
index 0000000..600ce9d5
--- /dev/null
+++ b/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8">
+<title>Autopip Video Sandboxed</title>
+</head>
+<body>
+  <video id="video" controls loop>
+    <source src="../bigbuck.webm">
+  </video>
+  <button id="pip" onclick="openPip()">Enter Picture-in-Picture</button>
+</body>
+<script>
+const video = document.getElementById('video');
+
+function playVideo() {
+  navigator.mediaSession.playbackState = "playing";
+  video.play();
+}
+
+function pauseVideo() {
+  video.pause();
+  navigator.mediaSession.playbackState = "paused";
+}
+
+function muteVideo() {
+  video.muted = true;
+}
+
+function openPip(details) {
+  video.requestPictureInPicture();
+}
+
+navigator.mediaSession.setActionHandler('enterpictureinpicture', openPip);
+</script>
diff --git a/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html.mock-http-headers b/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html.mock-http-headers
new file mode 100644
index 0000000..82539c23
--- /dev/null
+++ b/chrome/test/data/media/picture-in-picture/autopip-video-sandboxed.html.mock-http-headers
@@ -0,0 +1,2 @@
+HTTP/1.1 200 OK
+Content-Security-Policy: sandbox allow-scripts
\ No newline at end of file
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.