Low watch CVSS 4.3 chrome Spoofing 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in UI
DescriptionUI misrepresentation in UI
ComponentUI
Bug ClassSpoofing
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.
Tracker519246298
Fix commit5cfef9034728 (chromium/src) +151/-21
CWECWE-451
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-06-02 (per vendor advisory)
Fix landed2026-07-14 (public git)
Disclosed2026-08-25
Patch gap42 days — fix public in source before the advisory

Background

Global Media Controls (GMC)
the browser (and on ChromeOS, ash system-tray) UI listing active media sessions. Each session is a MediaSessionNotificationItem that shows an origin label and can offer a Cast footer or dialog.
Presentation context
when a frame calls the Presentation API, MediaNotificationService stores a media_router::StartPresentationContext in context_. When the user picks Cast on that session’s GMC item, MediaRouterUI::CreateWithStartPresentationContext consumes the context and starts the session for that request.
Presentation request origin override
UpdateMediaItemSourceOrigin / UpdatePresentationRequestOrigin make an item display the requesting frame’s origin (presentation_request().frame_origin) instead of the top-frame origin. The item stores it in optional_presentation_request_origin_.
Display-origin filtering
media_message_center::IsOriginGoodForDisplay rejects origins that should not be shown. UpdatePresentationRequestOrigin returns early for such origins.

Root Cause Analysis

The origin shown on a GMC media item and the presentation context that the Cast action actually uses had separate lifetimes that could fall out of sync. In MediaSessionNotificationItem, the branch that runs when the view is set to null (the removed else { optional_presentation_request_origin_.reset(); }, which the unit test ties to SetView(nullptr)) cleared the origin override whenever the GMC dialog closed. Meanwhile MediaNotificationService::context_ survived, so on reopening, the item showed the top-frame origin while a Cast click would still start the presentation request from the stored (possibly different) frame origin. In the other direction, MediaNotificationService dropped context_ (when it was consumed by CreateWithStartPresentationContext, or with a bare context_.reset() when the initiating RenderFrameHost was gone or inactive) without clearing the override, so a stale origin could stay on the item. Either way, the origin presented to the user did not reliably identify who the Cast action would act for.

The fix gives the override the same lifetime as context_. A new ResetPresentationContext() resets context_ and, using the newly tracked context_item_id_, pushes std::nullopt through MediaSessionItemProducer::UpdateMediaItemSourceOrigin (and the ash MediaNotificationProvider on ChromeOS). It is called when a new controllable-session request replaces the context, when the context is consumed for a cast dialog, and when the initiator frame is inactive. The view-null reset in the item was removed, and the origin parameter became std::optional<url::Origin> along the whole chain so the override can be explicitly cleared.

Key insight
The origin label for a Cast-capable media item was tied to the dialog’s view lifecycle, while the presentation request it describes was tied to MediaNotificationService::context_, so the two could disagree. The fix makes context_ the single owner of the origin override and clears both together.

Attack Path

  1. Presentation request from a frame a page plays media with an active media session, and a frame in it (possibly a cross-origin subframe, whose frame_origin differs from the top frame) starts a Presentation API request. This typically requires a user gesture. MediaNotificationService stores context_ and sets the item’s origin override to that frame’s origin.
  2. Desynchronising the label the user closes the GMC dialog, which before the fix reset the override in MediaSessionNotificationItem while context_ stayed alive. Alternatively, the context was consumed or its frame went away while the old override stayed.
  3. Misleading GMC item when GMC is reopened, the item shows the top-frame origin (or a stale origin) instead of the origin that owns the pending presentation request.
  4. User acts on wrong attribution the user chooses to Cast based on the displayed origin, but the stored context_ from the other origin is what CreateWithStartPresentationContext consumes, so casting starts on behalf of an origin the user was not shown (inferred from the lifecycle mismatch, since the patch does not show the downstream presentation flow).

Impact Assessment

The advisory classifies this as UI misrepresentation: browser UI (GMC, and the ChromeOS system-tray media UI) could attribute a pending Cast/presentation request to the wrong origin, undermining the origin-based consent the dialog relies on. Exploitation needs the user to interact with GMC and with a page that issues a Presentation API request while media is playing, and it depends on device/Cast availability. There is no memory-corruption or process-boundary impact. Any resulting unwanted presentation to a sink on behalf of an unexpected origin is (inferred). No exploited-in-the-wild status is given.

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/ash/global_media_controls/media_notification_provider_impl.cc
modified
TEST_F
chrome/browser/ui/global_media_controls/media_notification_service_unittest.cc
modified
TEST_F
components/global_media_controls/public/media_session_notification_item_unittest.cc
modified

Files Changed

  • chrome/browser/ui/global_media_controls/media_notification_service.cc
  • components/global_media_controls/public/media_session_notification_item.h
  • components/global_media_controls/public/media_session_item_producer.cc
  • components/global_media_controls/public/media_session_notification_item.cc
  • components/global_media_controls/public/media_session_notification_item_unittest.cc
  • chrome/browser/ui/ash/global_media_controls/media_notification_provider_impl.h
  • chrome/browser/ui/global_media_controls/media_notification_service_unittest.cc
  • ash/system/media/media_notification_provider.h
  • chrome/browser/ui/ash/global_media_controls/media_notification_provider_impl.cc
  • ash/system/media/mock_media_notification_provider.h
  • chrome/browser/ui/global_media_controls/media_notification_service.h
  • components/global_media_controls/public/media_session_item_producer.h

Audit Directions

  • Security labels with separate lifetimes
    look for UI that caches a security-relevant origin (permission prompts, Cast/Media Router dialogs, picker UIs, download bubbles) whose lifetime follows a view or dialog rather than the pending request it describes, and check that every path that consumes or drops the request also clears the label.
  • Bare `reset()` of request state
    grep for direct context_.reset()-style teardown of pending request objects that bypasses a helper responsible for related UI or state, as the old frame-inactive path did.
  • Subframe-origin attribution
    check other GMC, Media Router and Presentation API flows that show frame_origin versus top-frame origin, to make sure cross-origin subframe requests are attributed consistently across dialog close/reopen and multiple-tab scenarios.
From 5cfef9034728f584a838f2a93c959dd61a3a8333 Mon Sep 17 00:00:00 2001
From: Yiren Wang <[email protected]>
Date: Tue, 14 Jul 2026 16:20:47 -0700
Subject: [PATCH] Fix lifecycle of presentation context origin in GMC

We should not reset the origin for the presentation item whenever GMC
dialog is closed, since it can become out of sync with the origin in
MediaNotificationService. Instead, we always keep its lifecycle in sync
with MediaNotificationService.

- Allows resetting the origin override on a media session item by changing MediaSessionNotificationItem::UpdatePresentationRequestOrigin to accept std::optional<url::Origin>.

- Ensures the origin override persists when the notification view is temporarily set to null (e.g., when the GMC dialog is closed and reopened). It is only cleared when the presentation context itself is reset.

- Cleans up the active presentation context and clears the associated origin override in MediaNotificationService when:
  - The context is consumed by starting a cast session.
  - The initiating frame is destroyed or becomes inactive.
  - A new presentation request is initiated (including requests that delegate to a dummy presentation request notification).

Bug: 519246298
Change-Id: I5c6320e467977c2da0bdbb1e4920cd8193d4285d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8060464
Commit-Queue: Yiren Wang <[email protected]>
Reviewed-by: Ahmed Moussa <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1662248}
---
diff --git a/ash/system/media/media_notification_provider.h b/ash/system/media/media_notification_provider.h
index fdd43a8..93bfa86 100644
--- a/ash/system/media/media_notification_provider.h
+++ b/ash/system/media/media_notification_provider.h
@@ -98,8 +98,9 @@
 
   // Use MediaNotificationProvider as a bridge to update the source origin of a
   // media notification item.
-  virtual void UpdateMediaItemSourceOrigin(const std::string& id,
-                                           const url::Origin& origin) = 0;
+  virtual void UpdateMediaItemSourceOrigin(
+      const std::string& id,
+      const std::optional<url::Origin>& origin) = 0;
 };
 
 }  // namespace ash
diff --git a/ash/system/media/mock_media_notification_provider.h b/ash/system/media/mock_media_notification_provider.h
index 2310e70b..6eca5ed3 100644
--- a/ash/system/media/mock_media_notification_provider.h
+++ b/ash/system/media/mock_media_notification_provider.h
@@ -36,7 +36,7 @@
               ());
   MOCK_METHOD(void,
               UpdateMediaItemSourceOrigin,
-              (const std::string&, const url::Origin&),
+              (const std::string&, const std::optional<url::Origin>&),
               (override));
 
   void AddObserver(MediaNotificationProviderObserver* observer) override {}
diff --git a/chrome/browser/ui/ash/global_media_controls/media_notification_provider_impl.cc b/chrome/browser/ui/ash/global_media_controls/media_notification_provider_impl.cc
index ec7429d7..d8653fa 100644
--- a/chrome/browser/ui/ash/global_media_controls/media_notification_provider_impl.cc
+++ b/chrome/browser/ui/ash/global_media_controls/media_notification_provider_impl.cc
@@ -193,7 +193,7 @@
 
 void MediaNotificationProviderImpl::UpdateMediaItemSourceOrigin(
     const std::string& id,
-    const url::Origin& origin) {
+    const std::optional<url::Origin>& origin) {
   if (media_session_item_producer_) {
     media_session_item_producer_->UpdateMediaItemSourceOrigin(id, origin);
   }
diff --git a/chrome/browser/ui/ash/global_media_controls/media_notification_provider_impl.h b/chrome/browser/ui/ash/global_media_controls/media_notification_provider_impl.h
index 47a86bdc..939cca8 100644
--- a/chrome/browser/ui/ash/global_media_controls/media_notification_provider_impl.h
+++ b/chrome/browser/ui/ash/global_media_controls/media_notification_provider_impl.h
@@ -75,8 +75,9 @@
   std::unique_ptr<global_media_controls::MediaItemUIFooter> BuildFooterView(
       const std::string& id,
       base::WeakPtr<media_message_center::MediaNotificationItem> item) override;
-  void UpdateMediaItemSourceOrigin(const std::string& id,
-                                   const url::Origin& origin) override;
+  void UpdateMediaItemSourceOrigin(
+      const std::string& id,
+      const std::optional<url::Origin>& origin) override;
 
   // global_media_controls::MediaDialogDelegate:
   global_media_controls::MediaItemUI* ShowMediaItem(
diff --git a/chrome/browser/ui/global_media_controls/media_notification_service.cc b/chrome/browser/ui/global_media_controls/media_notification_service.cc
index fe2d536..bc3660e 100644
--- a/chrome/browser/ui/global_media_controls/media_notification_service.cc
+++ b/chrome/browser/ui/global_media_controls/media_notification_service.cc
@@ -447,6 +447,8 @@
       return;
     }
 
+    ResetPresentationContext();
+
     // If there exists a media session notification associated with
     // |web_contents|, hold onto the context for later use.
     context_ = std::move(context);
@@ -456,6 +458,7 @@
     // the top frame.
     std::string item_id =
         GetActiveControllableSessionForWebContents(web_contents);
+    context_item_id_ = item_id;
     media_session_item_producer_->UpdateMediaItemSourceOrigin(
         item_id, context_->presentation_request().frame_origin);
 #if BUILDFLAG(IS_CHROMEOS)
@@ -519,8 +522,11 @@
   }
 
   if (HasPresentationContextForSession(id)) {
-    return media_router::MediaRouterUI::CreateWithStartPresentationContext(
-        web_contents, std::move(context_));
+    auto dialog_controller =
+        media_router::MediaRouterUI::CreateWithStartPresentationContext(
+            web_contents, std::move(context_));
+    ResetPresentationContext();
+    return dialog_controller;
   }
 
   auto remote_playback_metadata =
@@ -602,7 +608,7 @@
   auto* initiator_rfh = content::RenderFrameHost::FromID(
       context_->presentation_request().render_frame_host_id);
   if (!initiator_rfh || !initiator_rfh->IsActive()) {
-    context_.reset();
+    ResetPresentationContext();
     return false;
   }
   auto* web_contents =
@@ -611,6 +617,20 @@
                              initiator_rfh) == web_contents;
 }
 
+void MediaNotificationService::ResetPresentationContext() {
+  context_.reset();
+  if (!context_item_id_.empty()) {
+    media_session_item_producer_->UpdateMediaItemSourceOrigin(context_item_id_,
+                                                              std::nullopt);
+#if BUILDFLAG(IS_CHROMEOS)
+    if (auto* provider = ash::MediaNotificationProvider::Get(); provider) {
+      provider->UpdateMediaItemSourceOrigin(context_item_id_, std::nullopt);
+    }
+#endif
+    context_item_id_.clear();
+  }
+}
+
 void MediaNotificationService::set_device_provider_for_testing(
     std::unique_ptr<MediaNotificationDeviceProvider> device_provider) {
   device_provider_ = std::move(device_provider);
diff --git a/chrome/browser/ui/global_media_controls/media_notification_service.h b/chrome/browser/ui/global_media_controls/media_notification_service.h
index 102ceba6..b0f2191 100644
--- a/chrome/browser/ui/global_media_controls/media_notification_service.h
+++ b/chrome/browser/ui/global_media_controls/media_notification_service.h
@@ -174,6 +174,9 @@
   // True if there is a presentation context for the given session ID.
   bool HasPresentationContextForSession(const std::string& session_id);
 
+  // Resets the presentation context and related states.
+  void ResetPresentationContext();
+
   // True if there are cast notifications associated with |web_contents|.
   bool HasCastNotificationsForWebContents(
       content::WebContents* web_contents) const;
@@ -212,6 +215,9 @@
   // Used to initialize a MediaRouterUI.
   std::unique_ptr<media_router::StartPresentationContext> context_;
 
+  // The ID of the media session item currently associated with `context_`.
+  std::string context_item_id_;
+
   // Generates a list of available audio devices.
   std::unique_ptr<MediaNotificationDeviceProvider> device_provider_;
 
diff --git a/components/global_media_controls/public/media_session_item_producer.cc b/components/global_media_controls/public/media_session_item_producer.cc
index 68ee3060..6d273a7 100644
--- a/components/global_media_controls/public/media_session_item_producer.cc
+++ b/components/global_media_controls/public/media_session_item_producer.cc
@@ -441,7 +441,7 @@
 
 void MediaSessionItemProducer::UpdateMediaItemSourceOrigin(
     const std::string& id,
-    const url::Origin& origin) {
+    const std::optional<url::Origin>& origin) {
   auto it = sessions_.find(id);
   if (it != sessions_.end())
     it->second.item()->UpdatePresentationRequestOrigin(origin);
diff --git a/components/global_media_controls/public/media_session_item_producer.h b/components/global_media_controls/public/media_session_item_producer.h
index c10b413..9ce9b83 100644
--- a/components/global_media_controls/public/media_session_item_producer.h
+++ b/components/global_media_controls/public/media_session_item_producer.h
@@ -106,7 +106,7 @@
   // as to show the origin associated with the request rather than that for the
   // top frame.
   void UpdateMediaItemSourceOrigin(const std::string& id,
-                                   const url::Origin& origin);
+                                   const std::optional<url::Origin>& origin);
 
  private:
   friend class MediaSessionItemProducerTest;
diff --git a/components/global_media_controls/public/media_session_notification_item.cc b/components/global_media_controls/public/media_session_notification_item.cc
index b48cf0b..e720b1f 100644
--- a/components/global_media_controls/public/media_session_notification_item.cc
+++ b/components/global_media_controls/public/media_session_notification_item.cc
@@ -155,8 +155,8 @@
 }
 
 void MediaSessionNotificationItem::UpdatePresentationRequestOrigin(
-    const url::Origin& origin) {
-  if (!media_message_center::IsOriginGoodForDisplay(origin)) {
+    const std::optional<url::Origin>& origin) {
+  if (origin && !media_message_center::IsOriginGoodForDisplay(*origin)) {
     return;
   }
 
@@ -223,8 +223,6 @@
     }
     if (session_favicon_.has_value())
       view_->UpdateWithFavicon(*session_favicon_);
-  } else {
-    optional_presentation_request_origin_.reset();
   }
 }
 
diff --git a/components/global_media_controls/public/media_session_notification_item.h b/components/global_media_controls/public/media_session_notification_item.h
index 76b69fc..a491ac2 100644
--- a/components/global_media_controls/public/media_session_notification_item.h
+++ b/components/global_media_controls/public/media_session_notification_item.h
@@ -88,9 +88,15 @@
       const std::optional<media_session::MediaPosition>& position) override;
 
   // Called when a media session item is associated with a presentation request
-  // to show the origin associated with the request rather than that for the
-  // top frame.
-  void UpdatePresentationRequestOrigin(const url::Origin& origin);
+  // to show the origin associated with the request rather than that for the top
+  // frame.
+  void UpdatePresentationRequestOrigin(
+      const std::optional<url::Origin>& origin);
+
+  const std::optional<url::Origin>& optional_presentation_request_origin()
+      const {
+    return optional_presentation_request_origin_;
+  }
 
   // Called during the creation of the footer view to show / set sink name if
   // there is an active casting session associated with `this` media item.
diff --git a/chrome/browser/ui/global_media_controls/media_notification_service_unittest.cc b/chrome/browser/ui/global_media_controls/media_notification_service_unittest.cc
index cf899d52..cddfd5b3 100644
--- a/chrome/browser/ui/global_media_controls/media_notification_service_unittest.cc
+++ b/chrome/browser/ui/global_media_controls/media_notification_service_unittest.cc
@@ -212,6 +212,10 @@
                                         url::Origin::Create(GURL()));
   }
 
+  bool HasPresentationContextForSession(const std::string& session_id) {
+    return service()->HasPresentationContextForSession(session_id);
+  }
+
   std::unique_ptr<StartPresentationContext> CreateStartPresentationContext(
       content::PresentationRequest presentation_request,
       StartPresentationContext::PresentationConnectionCallback success_cb =
@@ -765,3 +769,93 @@
   service()->OnSinksDiscovered(id.ToString());
   EXPECT_TRUE(service()->should_show_cast_local_media_iph());
 }
+
+TEST_F(MediaNotificationServiceCastTest, PresentationRequestOriginLifecycle) {
+  auto id = SimulatePlayingControllableMediaForWebContents(web_contents());
+  auto* item = GetNotificationSessionItem(id);
+  ASSERT_TRUE(item);
+  EXPECT_FALSE(item->optional_presentation_request_origin().has_value());
+
+  content::PresentationRequest presentation_request(
+      main_rfh()->GetGlobalId(),
+      {GURL("https://example.com"), GURL("https://example.com")},
+      url::Origin::Create(GURL("https://example.com")));
+  auto context = CreateStartPresentationContext(presentation_request);
+  auto origin = context->presentation_request().frame_origin;
+  service()->OnStartPresentationContextCreated(std::move(context));
+
+  // The origin override is set and the context is alive.
+  EXPECT_EQ(item->optional_presentation_request_origin(), origin);
+  EXPECT_TRUE(HasPresentationContextForSession(id.ToString()));
+
+  // Creating the cast dialog controller consumes the context, so the origin
+  // override is reset.
+  auto dialog_controller = CreateCastDialogControllerForSession(id.ToString());
+  EXPECT_FALSE(item->optional_presentation_request_origin().has_value());
+}
+
+TEST_F(MediaNotificationServiceCastTest,
+       PresentationRequestOriginNotResetByNewDummyRequest) {
+  auto id = SimulatePlayingControllableMediaForWebContents(web_contents());
+  auto* item = GetNotificationSessionItem(id);
+  ASSERT_TRUE(item);
+  EXPECT_FALSE(item->optional_presentation_request_origin().has_value());
+
+  content::PresentationRequest presentation_request1(
+      main_rfh()->GetGlobalId(),
+      {GURL("https://example.com"), GURL("https://example.com")},
+      url::Origin::Create(GURL("https://example.com")));
+  auto context1 = CreateStartPresentationContext(presentation_request1);
+  auto origin1 = context1->presentation_request().frame_origin;
+  service()->OnStartPresentationContextCreated(std::move(context1));
+
+  // The origin override is set and the context is alive.
+  EXPECT_EQ(item->optional_presentation_request_origin(), origin1);
+  EXPECT_TRUE(HasPresentationContextForSession(id.ToString()));
+
+  // Create a second WebContents and a second presentation request for it.
+  // There is no controllable media on this second WebContents.
+  std::unique_ptr<content::WebContents> web_contents2(CreateTestWebContents());
+  content::PresentationRequest presentation_request2(
+      web_contents2->GetPrimaryMainFrame()->GetGlobalId(),
+      {GURL("https://example2.com"), GURL("https://example2.com")},
+      url::Origin::Create(GURL("https://example2.com")));
+  auto context2 = CreateStartPresentationContext(presentation_request2);
+
+  // When we send the second request, since it doesn't have an active
+  // controllable session on web_contents2, it should go to
+  // presentation_request_notification_producer_ and we expect the previous
+  // presentation context for web_contents1 to remain intact.
+  service()->OnStartPresentationContextCreated(std::move(context2));
+
+  // The origin override on the first item should remain unchanged.
+  EXPECT_EQ(item->optional_presentation_request_origin(), origin1);
+  EXPECT_TRUE(HasPresentationContextForSession(id.ToString()));
+}
+
+TEST_F(MediaNotificationServiceCastTest,
+       HasPresentationContextForSessionWithInactiveFrame) {
+  auto id = SimulatePlayingControllableMediaForWebContents(web_contents());
+  auto* item = GetNotificationSessionItem(id);
+  ASSERT_TRUE(item);
+  EXPECT_FALSE(item->optional_presentation_request_origin().has_value());
+
+  content::PresentationRequest presentation_request(
+      main_rfh()->GetGlobalId(),
+      {GURL("https://example.com"), GURL("https://example.com")},
+      url::Origin::Create(GURL("https://example.com")));
+  auto context = CreateStartPresentationContext(presentation_request);
+  auto origin = context->presentation_request().frame_origin;
+  service()->OnStartPresentationContextCreated(std::move(context));
+
+  // The origin override is set and the context is alive.
+  EXPECT_EQ(item->optional_presentation_request_origin(), origin);
+  EXPECT_TRUE(HasPresentationContextForSession(id.ToString()));
+
+  // Simulate the WebContents being deleted.
+  DeleteContents();
+
+  // The session no longer has a presentation context because the initiating
+  // frame is deleted.
+  EXPECT_FALSE(HasPresentationContextForSession(id.ToString()));
+}
diff --git a/components/global_media_controls/public/media_session_notification_item_unittest.cc b/components/global_media_controls/public/media_session_notification_item_unittest.cc
index d6557de..7388f6a 100644
--- a/components/global_media_controls/public/media_session_notification_item_unittest.cc
+++ b/components/global_media_controls/public/media_session_notification_item_unittest.cc
@@ -119,10 +119,14 @@
   item().MediaSessionMetadataChanged(metadata);
   item().SetView(nullptr);
 
-  // Make sure that presentation request origin was reset after the view is set
-  // to null in SetView().
-  EXPECT_CALL(view(), UpdateWithMediaMetadata(metadata)).Times(1);
+  // Make sure that presentation request origin persists after the view is set
+  // to null and then set back to non-null.
+  EXPECT_CALL(view(), UpdateWithMediaMetadata(updated_metadata)).Times(1);
   item().SetView(&view());
+
+  // Make sure that presentation request origin can be reset.
+  EXPECT_CALL(view(), UpdateWithMediaMetadata(metadata)).Times(1);
+  item().UpdatePresentationRequestOrigin(std::nullopt);
 }
 
 TEST_F(MediaSessionNotificationItemTest, Freezing_DoNotUpdateImage) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/global_media_controls/media_notification_service_unittest.cc b/chrome/browser/ui/global_media_controls/media_notification_service_unittest.cc
index cf899d52..cddfd5b3 100644
--- a/chrome/browser/ui/global_media_controls/media_notification_service_unittest.cc
+++ b/chrome/browser/ui/global_media_controls/media_notification_service_unittest.cc
@@ -212,6 +212,10 @@
                                         url::Origin::Create(GURL()));
   }
 
+  bool HasPresentationContextForSession(const std::string& session_id) {
+    return service()->HasPresentationContextForSession(session_id);
+  }
+
   std::unique_ptr<StartPresentationContext> CreateStartPresentationContext(
       content::PresentationRequest presentation_request,
       StartPresentationContext::PresentationConnectionCallback success_cb =
@@ -765,3 +769,93 @@
   service()->OnSinksDiscovered(id.ToString());
   EXPECT_TRUE(service()->should_show_cast_local_media_iph());
 }
+
+TEST_F(MediaNotificationServiceCastTest, PresentationRequestOriginLifecycle) {
+  auto id = SimulatePlayingControllableMediaForWebContents(web_contents());
+  auto* item = GetNotificationSessionItem(id);
+  ASSERT_TRUE(item);
+  EXPECT_FALSE(item->optional_presentation_request_origin().has_value());
+
+  content::PresentationRequest presentation_request(
+      main_rfh()->GetGlobalId(),
+      {GURL("https://example.com"), GURL("https://example.com")},
+      url::Origin::Create(GURL("https://example.com")));
+  auto context = CreateStartPresentationContext(presentation_request);
+  auto origin = context->presentation_request().frame_origin;
+  service()->OnStartPresentationContextCreated(std::move(context));
+
+  // The origin override is set and the context is alive.
+  EXPECT_EQ(item->optional_presentation_request_origin(), origin);
+  EXPECT_TRUE(HasPresentationContextForSession(id.ToString()));
+
+  // Creating the cast dialog controller consumes the context, so the origin
+  // override is reset.
+  auto dialog_controller = CreateCastDialogControllerForSession(id.ToString());
+  EXPECT_FALSE(item->optional_presentation_request_origin().has_value());
+}
+
+TEST_F(MediaNotificationServiceCastTest,
+       PresentationRequestOriginNotResetByNewDummyRequest) {
+  auto id = SimulatePlayingControllableMediaForWebContents(web_contents());
+  auto* item = GetNotificationSessionItem(id);
+  ASSERT_TRUE(item);
+  EXPECT_FALSE(item->optional_presentation_request_origin().has_value());
+
+  content::PresentationRequest presentation_request1(
+      main_rfh()->GetGlobalId(),
+      {GURL("https://example.com"), GURL("https://example.com")},
+      url::Origin::Create(GURL("https://example.com")));
+  auto context1 = CreateStartPresentationContext(presentation_request1);
+  auto origin1 = context1->presentation_request().frame_origin;
+  service()->OnStartPresentationContextCreated(std::move(context1));
+
+  // The origin override is set and the context is alive.
+  EXPECT_EQ(item->optional_presentation_request_origin(), origin1);
+  EXPECT_TRUE(HasPresentationContextForSession(id.ToString()));
+
+  // Create a second WebContents and a second presentation request for it.
+  // There is no controllable media on this second WebContents.
+  std::unique_ptr<content::WebContents> web_contents2(CreateTestWebContents());
+  content::PresentationRequest presentation_request2(
+      web_contents2->GetPrimaryMainFrame()->GetGlobalId(),
+      {GURL("https://example2.com"), GURL("https://example2.com")},
+      url::Origin::Create(GURL("https://example2.com")));
+  auto context2 = CreateStartPresentationContext(presentation_request2);
+
+  // When we send the second request, since it doesn't have an active
+  // controllable session on web_contents2, it should go to
+  // presentation_request_notification_producer_ and we expect the previous
+  // presentation context for web_contents1 to remain intact.
+  service()->OnStartPresentationContextCreated(std::move(context2));
+
+  // The origin override on the first item should remain unchanged.
+  EXPECT_EQ(item->optional_presentation_request_origin(), origin1);
+  EXPECT_TRUE(HasPresentationContextForSession(id.ToString()));
+}
+
+TEST_F(MediaNotificationServiceCastTest,
+       HasPresentationContextForSessionWithInactiveFrame) {
+  auto id = SimulatePlayingControllableMediaForWebContents(web_contents());
+  auto* item = GetNotificationSessionItem(id);
+  ASSERT_TRUE(item);
+  EXPECT_FALSE(item->optional_presentation_request_origin().has_value());
+
+  content::PresentationRequest presentation_request(
+      main_rfh()->GetGlobalId(),
+      {GURL("https://example.com"), GURL("https://example.com")},
+      url::Origin::Create(GURL("https://example.com")));
+  auto context = CreateStartPresentationContext(presentation_request);
+  auto origin = context->presentation_request().frame_origin;
+  service()->OnStartPresentationContextCreated(std::move(context));
+
+  // The origin override is set and the context is alive.
+  EXPECT_EQ(item->optional_presentation_request_origin(), origin);
+  EXPECT_TRUE(HasPresentationContextForSession(id.ToString()));
+
+  // Simulate the WebContents being deleted.
+  DeleteContents();
+
+  // The session no longer has a presentation context because the initiating
+  // frame is deleted.
+  EXPECT_FALSE(HasPresentationContextForSession(id.ToString()));
+}
diff --git a/components/global_media_controls/public/media_session_notification_item_unittest.cc b/components/global_media_controls/public/media_session_notification_item_unittest.cc
index d6557de..7388f6a 100644
--- a/components/global_media_controls/public/media_session_notification_item_unittest.cc
+++ b/components/global_media_controls/public/media_session_notification_item_unittest.cc
@@ -119,10 +119,14 @@
   item().MediaSessionMetadataChanged(metadata);
   item().SetView(nullptr);
 
-  // Make sure that presentation request origin was reset after the view is set
-  // to null in SetView().
-  EXPECT_CALL(view(), UpdateWithMediaMetadata(metadata)).Times(1);
+  // Make sure that presentation request origin persists after the view is set
+  // to null and then set back to non-null.
+  EXPECT_CALL(view(), UpdateWithMediaMetadata(updated_metadata)).Times(1);
   item().SetView(&view());
+
+  // Make sure that presentation request origin can be reset.
+  EXPECT_CALL(view(), UpdateWithMediaMetadata(metadata)).Times(1);
+  item().UpdatePresentationRequestOrigin(std::nullopt);
 }
 
 TEST_F(MediaSessionNotificationItemTest, Freezing_DoNotUpdateImage) {
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.