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 <[email protected]> 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 <[email protected]> Reviewed-by: Phil Yan <[email protected]> Commit-Queue: Jenna Himawan <[email protected]> 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