Chrome · UI › MediaSession
CVE-2026-106368
Spoofing in UI (MediaSession)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaSessionHelper.java |
modified |
Files Changed
services/media_session/public/cpp/android/junit/BUILD.gnchrome/android/junit/src/org/chromium/chrome/browser/media/ui/MediaNotificationTitleUpdatedTest.javacomponents/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaSessionHelper.javaservices/media_session/public/cpp/media_metadata_android.ccservices/media_session/public/cpp/android/junit/src/org/chromium/services/media_session/MediaMetadataUnitTest.javaservices/media_session/public/cpp/android/java/src/org/chromium/services/media_session/MediaMetadata.javacomponents/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaNotificationInfoTest.java
Patch
From 5d2240ac74ed1882652472bbb9975186740738f5 Mon Sep 17 00:00:00 2001
From: Jenna Himawan <jhimawan@google.com>
Date: Tue, 01 Sep 2026 14:56:26 -0700
Subject: [PATCH] [MediaSession] Plumb source title across JNI for Android media notifications
Previously, Android media notifications derived the origin label from
webContents.getVisibleUrl().getOrigin().getSpec() in
MediaSessionHelper. For about:blank, blob:, and opaque origins,
GURL.getOrigin() returns an empty origin, omitting origin attribution
in the media notification.
This change aligns Android with desktop by plumbing source_title
across JNI via MediaMetadata. MediaSessionImpl in C++ already
properly computes source_title from the primary main frame's
committed origin (including handling for about:blank, blob URLs, and
precursor origins). MediaSessionHelper now derives the notification
origin from metadata.getSourceTitle() instead of parsing the visible URL.
TAG=agy
CONV=01e82ac3-df9b-4deb-a74f-1da5e82b55cb
Bug: 553252261
Change-Id: I95149205d7a6621848cc18f7a31bfaff5fb419fb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8316747
Reviewed-by: Tommy Steimel <steimel@chromium.org>
Reviewed-by: Phil Yan <philyan@chromium.org>
Commit-Queue: Jenna Himawan <jhimawan@google.com>
Cr-Commit-Position: refs/heads/main@{#1690166}
---
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/media/ui/MediaNotificationTitleUpdatedTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/media/ui/MediaNotificationTitleUpdatedTest.java
index 766fc66..d6ada619 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/media/ui/MediaNotificationTitleUpdatedTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/media/ui/MediaNotificationTitleUpdatedTest.java
@@ -317,6 +317,66 @@
assertEquals("title2", getDisplayedTitle());
}
+ @Test
+ public void testOriginUpdatedFromSourceTitle() {
+ mTabHolder.simulateNavigation("about:blank", false);
+ mTabHolder.simulateMediaSessionStateChanged(true, false);
+ mTabHolder.simulateMediaSessionMetadataChanged(
+ new MediaMetadata("title2", "artist", "album", "example.com"));
+ mTabHolder.simulateMediaSessionActionsChanged(DEFAULT_ACTIONS);
+ advanceTimeByMillis(THROTTLE_MILLIS);
+
+ assertEquals("example.com", getController().mMediaNotificationInfo.origin);
+ }
+
+ @Test
+ public void testOriginUpdatedFromSourceTitle_NullMetadata() {
+ mTabHolder.simulateNavigation("about:blank", false);
+ mTabHolder.simulateMediaSessionStateChanged(true, false);
+ mTabHolder.simulateMediaSessionMetadataChanged(null);
+ mTabHolder.simulateMediaSessionActionsChanged(DEFAULT_ACTIONS);
+ advanceTimeByMillis(THROTTLE_MILLIS);
+
+ assertEquals("", getController().mMediaNotificationInfo.origin);
+ }
+
+ @Test
+ public void testFallbackTitleFromSourceTitle_WhenPageTitleEmpty() {
+ mTabHolder.simulateNavigation("about:blank", false);
+ mTabHolder.simulateTitleUpdated("");
+ mTabHolder.simulateMediaSessionStateChanged(true, false);
+ mTabHolder.simulateMediaSessionMetadataChanged(
+ new MediaMetadata("", "artist", "album", "example.com"));
+ mTabHolder.simulateMediaSessionActionsChanged(DEFAULT_ACTIONS);
+ advanceTimeByMillis(THROTTLE_MILLIS);
+
+ assertEquals("example.com", getController().mMediaNotificationInfo.origin);
+ assertEquals("example.com", getDisplayedTitle());
+ }
+
+ @Test
+ public void testFallbackTitleUpdatesWhenSourceTitleChanges_WhenPageTitleEmpty() {
+ mTabHolder.simulateNavigation("about:blank", false);
+ mTabHolder.simulateTitleUpdated("");
+ mTabHolder.simulateMediaSessionStateChanged(true, false);
+ mTabHolder.simulateMediaSessionMetadataChanged(
+ new MediaMetadata("", "artist", "album", "first.example.com"));
+ mTabHolder.simulateMediaSessionActionsChanged(DEFAULT_ACTIONS);
+ advanceTimeByMillis(THROTTLE_MILLIS);
+
+ assertEquals("first.example.com", getController().mMediaNotificationInfo.origin);
+ assertEquals("first.example.com", getDisplayedTitle());
+
+ // Metadata updates with new sourceTitle on untitled session.
+ mTabHolder.simulateMediaSessionMetadataChanged(
+ new MediaMetadata("", "artist", "album", "second.example.com"));
+ mTabHolder.simulateMediaSessionActionsChanged(DEFAULT_ACTIONS);
+ advanceTimeByMillis(THROTTLE_MILLIS);
+
+ assertEquals("second.example.com", getController().mMediaNotificationInfo.origin);
+ assertEquals("second.example.com", getDisplayedTitle());
+ }
+
private CharSequence getDisplayedTitle() {
Notification notification = getController().mNotificationBuilder.build();
ShadowNotification shadowNotification = Shadows.shadowOf(notification);
diff --git a/components/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaNotificationInfoTest.java b/components/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaNotificationInfoTest.java
index c1b9a1e..d0ed5abd 100644
--- a/components/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaNotificationInfoTest.java
+++ b/components/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaNotificationInfoTest.java
@@ -133,4 +133,22 @@
assertEquals(original.supportsStop(), copy.supportsStop());
assertEquals(original.mediaSessionActions, copy.mediaSessionActions);
}
+
+ @Test
+ public void testBuilderWithSourceTitle() {
+ MediaNotificationInfo.Builder builder = new MediaNotificationInfo.Builder();
+ builder.setOrigin("https://example.com");
+ builder.setMetadata(new MediaMetadata("title", "artist", "album", "example.com"));
+ builder.setListener(mListener);
+ builder.setInstanceId(0);
+ builder.setId(0);
+
+ MediaNotificationInfo info = builder.build();
+ assertNotNull(info);
+ assertEquals("https://example.com", info.origin);
+ assertEquals("title", info.metadata.getTitle());
+ assertEquals("artist", info.metadata.getArtist());
+ assertEquals("album", info.metadata.getAlbum());
+ assertEquals("example.com", info.metadata.getSourceTitle());
+ }
}
diff --git a/components/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaSessionHelper.java b/components/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaSessionHelper.java
index 6b6a5e0..623046a 100644
--- a/components/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaSessionHelper.java
+++ b/components/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaSessionHelper.java
@@ -32,7 +32,6 @@
import org.chromium.build.annotations.RequiresNonNull;
import org.chromium.components.browser_ui.media.MediaSessionUma.MediaSessionActionSource;
import org.chromium.components.favicon.LargeIconBridge;
-import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.content_public.browser.MediaSession;
import org.chromium.content_public.browser.MediaSessionObserver;
import org.chromium.content_public.browser.NavigationHandle;
@@ -70,7 +69,9 @@
private @Nullable Bitmap mPageMediaImage;
@VisibleForTesting public @Nullable Bitmap mFavicon;
private @Nullable Bitmap mCurrentMediaImage;
- private @Nullable String mOrigin;
+ // The origin of the media session. Defaults to an empty string to guarantee
+ // a non-null origin when constructing MediaNotificationInfo before the origin is resolved.
+ private String mOrigin = "";
private int mPreviousVolumeControlStream = AudioManager.USE_DEFAULT_STREAM_TYPE;
@VisibleForTesting public MediaNotificationInfo.@Nullable Builder mNotificationInfoBuilder;
// The fallback title if |mPageMetadata| is null or its title is empty.
@@ -323,15 +324,14 @@
return;
}
assumeNonNull(mWebContents);
- assumeNonNull(mOrigin);
- Intent contentIntent = mDelegate.createBringTabToFrontIntent();
-
- if (mFallbackTitle == null) mFallbackTitle = sanitizeMediaTitle(mOrigin);
+ // Initialize fallback title from origin if no page title was set yet.
+ maybeSetFallbackTitleFromOrigin(null);
mCurrentMetadata = getMetadata();
mCurrentMediaImage = getCachedNotificationImage();
rebaseMediaPosition(isPaused);
+ Intent contentIntent = mDelegate.createBringTabToFrontIntent();
mNotificationInfoBuilder =
mDelegate
.createMediaNotificationInfoBuilder()
@@ -370,6 +370,11 @@
@Override
public void mediaSessionMetadataChanged(@Nullable MediaMetadata metadata) {
mPageMetadata = metadata;
+ String previousOrigin = mOrigin;
+ mOrigin = (metadata != null) ? metadata.getSourceTitle() : "";
+ // If mFallbackTitle was previously tracking the old origin, update it to
+ // the new origin without overriding any page title set via titleWasSet().
+ maybeSetFallbackTitleFromOrigin(previousOrigin);
updateNotificationMetadata();
}
@@ -442,9 +447,7 @@
return;
}
- mOrigin =
- UrlFormatter.formatUrlForDisplayOmitSchemeOmitTrivialSubdomains(
- webContents.getVisibleUrl().getOrigin().getSpec());
+ mOrigin = "";
mFavicon = null;
mPageMediaImage = null;
mPageMetadata = null;
@@ -694,6 +697,18 @@
}
/**
+ * Updates {@link #mFallbackTitle} to track {@link #mOrigin} if uninitialized or if previously
+ * derived from {@code previousOrigin}. Preserves real page titles set via {@link #titleWasSet}.
+ */
+ private void maybeSetFallbackTitleFromOrigin(@Nullable String previousOrigin) {
+ if (TextUtils.isEmpty(mFallbackTitle)
+ || (!TextUtils.isEmpty(previousOrigin)
+ && TextUtils.equals(mFallbackTitle, sanitizeMediaTitle(previousOrigin)))) {
+ mFallbackTitle = sanitizeMediaTitle(mOrigin);
+ }
+ }
+
+ /**
* Updates the metadata in media notification. This method should be called whenever
* |mPageMetadata| or |mFallbackTitle| is changed.
*/
@@ -705,6 +720,7 @@
mCurrentMetadata = newMetadata;
mNotificationInfoBuilder.setMetadata(mCurrentMetadata);
+ mNotificationInfoBuilder.setOrigin(mOrigin);
showNotification();
}
@@ -716,7 +732,11 @@
private MediaMetadata getMetadata() {
String artist = "";
String album = "";
+ String sourceTitle = mOrigin;
if (mPageMetadata != null) {
+ if (!TextUtils.isEmpty(mPageMetadata.getSourceTitle())) {
+ sourceTitle = mPageMetadata.getSourceTitle();
+ }
if (!TextUtils.isEmpty(mPageMetadata.getTitle())) return mPageMetadata;
artist = mPageMetadata.getArtist();
@@ -726,11 +746,12 @@
if (mCurrentMetadata != null
&& TextUtils.equals(mFallbackTitle, mCurrentMetadata.getTitle())
&& TextUtils.equals(artist, mCurrentMetadata.getArtist())
- && TextUtils.equals(album, mCurrentMetadata.getAlbum())) {
+ && TextUtils.equals(album, mCurrentMetadata.getAlbum())
+ && TextUtils.equals(sourceTitle, mCurrentMetadata.getSourceTitle())) {
return mCurrentMetadata;
}
- return new MediaMetadata(mFallbackTitle, artist, album);
+ return new MediaMetadata(mFallbackTitle, artist, album, sourceTitle);
}
private void updateNotificationActions() {
diff --git a/services/media_session/public/cpp/android/java/src/org/chromium/services/media_session/MediaMetadata.java b/services/media_session/public/cpp/android/java/src/org/chromium/services/media_session/MediaMetadata.java
index c5e6c8a..5f7a650d 100644
--- a/services/media_session/public/cpp/android/java/src/org/chromium/services/media_session/MediaMetadata.java
+++ b/services/media_session/public/cpp/android/java/src/org/chromium/services/media_session/MediaMetadata.java
@@ -4,8 +4,6 @@
package org.chromium.services.media_session;
-import static org.chromium.build.NullUtil.assumeNonNull;
-
import android.text.TextUtils;
import org.jni_zero.CalledByNative;
@@ -27,6 +25,8 @@
private String mAlbum;
+ private final String mSourceTitle;
+
/** Returns the title associated with the media session. */
public @Nullable String getTitle() {
return mTitle;
@@ -42,8 +42,14 @@
return mAlbum;
}
+ /** Returns the source title (origin/app name) associated with the media session. */
+ public String getSourceTitle() {
+ return mSourceTitle;
+ }
+
/**
* Sets the title associated with the media session.
+ *
* @param title The title to use for the media session.
*/
public void setTitle(@Nullable String title) {
@@ -51,8 +57,9 @@
}
/**
- * Sets the arstist name associated with the media session.
- * @param arstist The artist name to use for the media session.
+ * Sets the artist name associated with the media session.
+ *
+ * @param artist The artist name to use for the media session.
*/
public void setArtist(String artist) {
mArtist = artist;
@@ -67,19 +74,26 @@
}
/**
- * Creates a new MediaMetadata from the C++ code. This is exactly like the
- * constructor below apart that it can be called by native code.
+ * Creates a new MediaMetadata from C++ code. This is equivalent to the constructor below,
+ * except that it can be called by native code.
*/
@CalledByNative
- private static MediaMetadata create(@Nullable String title, String artist, String album) {
- return new MediaMetadata(title, artist, album);
+ private static MediaMetadata create(
+ @Nullable String title, String artist, String album, String sourceTitle) {
+ return new MediaMetadata(title, artist, album, sourceTitle);
}
/** Creates a new MediaMetadata. */
public MediaMetadata(@Nullable String title, String artist, String album) {
+ this(title, artist, album, "");
+ }
+
+ /** Creates a new MediaMetadata with source title. */
+ public MediaMetadata(@Nullable String title, String artist, String album, String sourceTitle) {
mTitle = title;
mArtist = artist;
mAlbum = album;
+ mSourceTitle = sourceTitle;
}
/** Comparing MediaMetadata is expensive and should be used sparingly */
@@ -91,19 +105,20 @@
MediaMetadata other = (MediaMetadata) obj;
return TextUtils.equals(mTitle, other.mTitle)
&& TextUtils.equals(mArtist, other.mArtist)
- && TextUtils.equals(mAlbum, other.mAlbum);
+ && TextUtils.equals(mAlbum, other.mAlbum)
+ && TextUtils.equals(mSourceTitle, other.mSourceTitle);
}
/**
* @return The hash code of this {@link MediaMetadata}. The method uses the same algorithm in
- * {@link java.util.List} for combinine hash values.
+ * {@link java.util.List} for combining hash values.
*/
@Override
public int hashCode() {
- assumeNonNull(mTitle);
- int result = mTitle.hashCode();
+ int result = mTitle != null ? mTitle.hashCode() : 0;
result = 31 * result + mArtist.hashCode();
result = 31 * result + mAlbum.hashCode();
+ result = 31 * result + mSourceTitle.hashCode();
return result;
}
}
diff --git a/services/media_session/public/cpp/android/junit/src/org/chromium/services/media_session/MediaMetadataUnitTest.java b/services/media_session/public/cpp/android/junit/src/org/chromium/services/media_session/MediaMetadataUnitTest.java
new file mode 100644
index 0000000..0f31bb9
--- /dev/null
+++ b/services/media_session/public/cpp/android/junit/src/org/chromium/services/media_session/MediaMetadataUnitTest.java
@@ -0,0 +1,84 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.services.media_session;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+
+/** Unit tests for {@link MediaMetadata}. */
+@RunWith(BaseRobolectricTestRunner.class)
+public class MediaMetadataUnitTest {
+ @Test
+ public void testDefaultConstructor() {
+ MediaMetadata metadata = new MediaMetadata("title", "artist", "album");
+ assertEquals("title", metadata.getTitle());
+ assertEquals("artist", metadata.getArtist());
+ assertEquals("album", metadata.getAlbum());
+ assertEquals("", metadata.getSourceTitle());
+ }
+
+ @Test
+ public void testConstructorWithSourceTitle() {
+ MediaMetadata metadata = new MediaMetadata("title", "artist", "album", "sourceTitle");
+ assertEquals("title", metadata.getTitle());
+ assertEquals("artist", metadata.getArtist());
+ assertEquals("album", metadata.getAlbum());
+ assertEquals("sourceTitle", metadata.getSourceTitle());
+ }
+
+ @Test
+ public void testSetters() {
+ MediaMetadata metadata = new MediaMetadata("title", "artist", "album", "sourceTitle");
+ metadata.setTitle("newTitle");
+ metadata.setArtist("newArtist");
+ metadata.setAlbum("newAlbum");
... (file diff truncated)
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