Chrome · Document Picture-in-Picture
CVE-2026-14141
Logic Error in Document Picture-in-Picture
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/PopupCreatorImpl.java |
modified | |
ifchrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/PopupCreatorImpl.javachrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.javachrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
Patch
From a973f1e01606c63519e867535192ca0abbbaf62f Mon Sep 17 00:00:00 2001
From: Phil Yan <philyan@chromium.org>
Date: Thu, 28 May 2026 15:57:02 -0700
Subject: [PATCH] [Doc-PiP] Prevent origin spoofing via opener navigation race
An attacker can request a Document PiP window on Android and
immediately navigate the opener window. Due to the asynchronous
startup sequence of DocumentPictureInPictureActivity, there is a
100-500ms delay before the native WebContentsObserver is
registered. This causes the browser to miss the navigation and
display the navigated victim's origin in the trusted PiP header.
To address this:
1. Register the native observer early inside AddNewContents. This
ensures opener navigations are caught immediately from the
moment the PiP window is requested.
2. Pass the opener's serialized origin at launch time in the intent
and verify it in performPreInflationStartup() and onStart(). On
mismatch, close the window. Using serialized origin prevents
Binder crashes on large URLs and supports opaque origin
verification.
3. Make the native registration in OnActivityStart completely
test-only (onActivityStartForTesting).
Bug: 514072867
Fix: 514072867
Change-Id: Id98308652cdf4c0fb2dea6e63b25b13fbffdcdc5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7876369
Commit-Queue: Phil Yan <philyan@chromium.org>
Reviewed-by: Frank Liberato <liberato@chromium.org>
Reviewed-by: Wenyu Fu <wenyufu@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1638015}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/PopupCreatorImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/PopupCreatorImpl.java
index ffaac6a..a75d5a2 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/PopupCreatorImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/PopupCreatorImpl.java
@@ -55,6 +55,8 @@
import org.chromium.ui.display.DisplayUtil;
import org.chromium.ui.insets.InsetObserver;
import org.chromium.ui.insets.WindowInsetsUtils;
+import org.chromium.url.GURL;
+import org.chromium.url.Origin;
/** Handles launching new popup windows as CCTs and Document Picture-in-Picture windows. */
@NullMarked
@@ -502,6 +504,17 @@
intent.putExtra(
DocumentPictureInPictureActivity.WINDOW_OPTIONS_KEY, windowOptions.toBundle());
+ // Record the opener's origin at the time of the request to prevent origin spoofing
+ // if the opener navigates before the Activity completes its launch.
+ WebContents opener = webContents.getDocumentPictureInPictureOpener();
+ if (opener != null) {
+ GURL openerUrl = opener.getLastCommittedUrl();
+ Origin openerOrigin = Origin.create(openerUrl != null ? openerUrl : GURL.emptyGURL());
+ intent.putExtra(
+ DocumentPictureInPictureActivity.INITIAL_OPENER_ORIGIN_KEY,
+ openerOrigin.toString());
+ }
+
intent.setAction(Intent.ACTION_VIEW);
return intent;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
index 9fa3c4e9..96e88dd 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
@@ -81,6 +81,7 @@
import org.chromium.ui.display.DisplayUtil;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.url.GURL;
+import org.chromium.url.Origin;
@NullMarked
public class DocumentPictureInPictureActivity extends AsyncInitializationActivity
@@ -92,6 +93,8 @@
"org.chromium.chrome.browser.media.DocumentPictureInPicture.WebContents";
public static final String WINDOW_OPTIONS_KEY =
"org.chromium.chrome.browser.media.DocumentPictureInPicture.WindowOptions";
+ public static final String INITIAL_OPENER_ORIGIN_KEY =
+ "org.chromium.chrome.browser.media.DocumentPictureInPicture.InitialOpenerOrigin";
private static final String IS_FROM_ACTIVITY_RECREATION_KEY =
"org.chromium.chrome.browser.media.DocumentPictureInPicture.IsFromActivityRecreation";
private WebContents mWebContents;
@@ -155,6 +158,11 @@
}
mParentWebContents = parentWebContents;
+ if (!verifyOpenerOrigin(intent, parentWebContents)) {
+ finish();
+ return;
+ }
+
Bundle windowOptionsBundle =
getWindowOptionsBundleFromInstanceStateOrIntent(intent, savedInstanceState);
if (windowOptionsBundle == null) {
@@ -207,9 +215,9 @@
super.onStart();
assert isContentsInitialized();
- if (!mIsFromActivityRecreation) {
- DocumentPictureInPictureActivityJni.get()
- .onActivityStart(mParentWebContents, mWebContents);
+ if (!verifyOpenerOrigin(getIntent(), mParentWebContents)) {
+ finish();
+ return;
}
mInitiatorTabObserver =
@@ -832,6 +840,48 @@
}
/**
+ * Enters Picture-in-Picture mode for testing. This is intended for test environments that
+ * launch the Activity directly.
+ */
+ public static void onActivityStartForTesting(
+ WebContents parentWebContents, WebContents webContents) {
+ DocumentPictureInPictureActivityJni.get()
+ .onActivityStartForTesting(parentWebContents, webContents); // IN-TEST
+ }
+
+ /**
+ * Verifies that the current opener's origin matches the origin captured when the PiP window was
+ * requested. This protects against a race condition where the opener window navigates during
+ * the asynchronous Activity startup.
+ *
+ * @param intent The launch intent.
+ * @param parentWebContents The opener's WebContents.
+ * @return True if the origins match, or if verification is skipped; false on mismatch.
+ */
+ private boolean verifyOpenerOrigin(Intent intent, WebContents parentWebContents) {
+ if (mIsFromActivityRecreation) {
+ return true; // Already verified on initial startup.
+ }
+ final String initialOpenerOriginStr = intent.getStringExtra(INITIAL_OPENER_ORIGIN_KEY);
+ if (initialOpenerOriginStr == null) {
+ Log.e(TAG, "No initial opener origin in intent! Finishing.");
+ return false;
+ }
+ final GURL currentOpenerUrl = parentWebContents.getLastCommittedUrl();
+ final String currentOpenerOriginStr = Origin.create(currentOpenerUrl).toString();
+ if (!initialOpenerOriginStr.equals(currentOpenerOriginStr)) {
+ Log.e(
+ TAG,
+ "Opener origin mismatch! Initial: "
+ + initialOpenerOriginStr
+ + ", Current: "
+ + currentOpenerOriginStr);
+ return false;
+ }
+ return true;
+ }
+
+ /**
* Sets the parent WebContents directly on this instance for testing. Use this in unit tests
* where the activity is created without running the full startup flow.
*/
@@ -854,7 +904,8 @@
@NativeMethods
public interface Natives {
- void onActivityStart(WebContents parentWebContent, WebContents webContents);
+ void onActivityStartForTesting( // IN-TEST
+ WebContents parentWebContent, WebContents webContents);
void onBackToTab();
}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
index 6ff9bb1..e3ea963b 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
@@ -45,6 +45,7 @@
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.test.util.JavaScriptUtils;
+import org.chromium.url.Origin;
/** Tests for DocumentPictureInPictureActivity. */
@RunWith(ChromeJUnit4ClassRunner.class)
@@ -65,7 +66,7 @@
@Before
public void setUp() {
- mActivityTestRule.startOnBlankPage();
+ mActivityTestRule.startOnTestServerUrl("/chrome/test/data/android/simple.html");
mTab = mActivityTestRule.getActivityTab();
mParentWebContents = mTab.getWebContents();
@@ -81,6 +82,11 @@
DocumentPictureInPictureActivity.setWebContentsForTesting(mWebContents);
DocumentPictureInPictureActivity.setParentWebContentsForTesting(mParentWebContents);
DocumentPictureInPictureActivity.setIgnoreSdkVersionForTesting(true);
+ ThreadUtils.runOnUiThreadBlocking(
+ () -> {
+ DocumentPictureInPictureActivity.onActivityStartForTesting(
+ mParentWebContents, mWebContents);
+ });
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