Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Media Session
DescriptionInappropriate implementation in Media Session
ComponentMedia Session
Bug ClassLogic Error
Tracker502633299
Fix commit650191ec14be (chromium/src) +97/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
content/browser/media/session/media_session_impl.cc
modified
BindLambdaForTesting
content/browser/media/session/media_session_impl_browsertest.cc
modified
MediaSessionImplPrerenderingBrowserTest
content/browser/media/session/media_session_impl_browsertest.cc
modified

Files Changed

  • content/browser/media/session/media_session_impl.cc
  • content/browser/media/session/media_session_impl_browsertest.cc
From 650191ec14be8e44d50cb143ee883aec4c898b4f Mon Sep 17 00:00:00 2001
From: Tommy Steimel <[email protected]>
Date: Tue, 21 Apr 2026 14:35:42 -0700
Subject: [PATCH] [Media Session] Download artwork in the correct frame

Currently, MediaSessionImpl always downloads artwork from the main
frame (which will request the artwork with the main frame's cookies).
This CL changes this to always request the artwork from the frame which
set the artwork URL (i.e. the currently routed frame).

This only applies to album/chapter artwork: we still download the
favicon image from the top frame.

Bug: 502633299
Change-Id: If6e5df6ed3635bbbed21e1ce49f7759c99baa6e5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7773179
Reviewed-by: Frank Liberato <[email protected]>
Commit-Queue: Tommy Steimel <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1618448}
---

diff --git a/content/browser/media/session/media_session_impl.cc b/content/browser/media/session/media_session_impl.cc
index e6c0c697..c2e6a328 100644
--- a/content/browser/media/session/media_session_impl.cc
+++ b/content/browser/media/session/media_session_impl.cc
@@ -1433,9 +1433,21 @@
     }
   }
 
+  // If we're downloading an image that isn't the favicon, then we should
+  // download it from the frame which set the artwork URL. We download favicon
+  // images from the main frame.
+  GlobalRenderFrameHostId frame_for_download;
+  if (!source_icon) {
+    if (!routed_service_) {
+      std::move(callback).Run(SkBitmap());
+      return;
+    }
+    frame_for_download = routed_service_->GetRenderFrameHostId();
+  }
+
   const gfx::Size preferred_size(desired_size_px, desired_size_px);
-  web_contents()->DownloadImage(
-      image.src, false /* is_favicon */, preferred_size,
+  web_contents()->DownloadImageInFrame(
+      frame_for_download, image.src, false /* is_favicon */, preferred_size,
       desired_size_px /* max_bitmap_size */, false /* bypass_cache */,
       base::BindOnce(&MediaSessionImpl::OnImageDownloadComplete,
                      base::Unretained(this),
diff --git a/content/browser/media/session/media_session_impl_browsertest.cc b/content/browser/media/session/media_session_impl_browsertest.cc
index b05a000..4655fe39 100644
--- a/content/browser/media/session/media_session_impl_browsertest.cc
+++ b/content/browser/media/session/media_session_impl_browsertest.cc
@@ -17,6 +17,7 @@
 #include "base/run_loop.h"
 #include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
@@ -384,10 +385,21 @@
  protected:
   std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
       const net::test_server::HttpRequest& request) {
+    last_request_path_ = request.relative_url;
+    auto it = request.headers.find("Referer");
+    if (it != request.headers.end()) {
+      last_request_referer_ = it->second;
+    } else {
+      last_request_referer_.clear();
+    }
+
     get_favicon_calls();
     return std::make_unique<net::test_server::BasicHttpResponse>();
   }
 
+  std::string last_request_path_;
+  std::string last_request_referer_;
+
   raw_ptr<MediaSessionImpl> media_session_ = nullptr;
   raw_ptr<MockAudioFocusDelegate> mock_audio_focus_delegate_ = nullptr;
   std::unique_ptr<MockMediaSessionServiceImpl> mock_media_session_service_;
@@ -3305,6 +3317,77 @@
 }
 #endif  // !BUILDFLAG(IS_ANDROID)
 
+IN_PROC_BROWSER_TEST_F(MediaSessionImplBrowserTest,
+                       DownloadArtworkFromCorrectFrame) {
+  // Navigate to a page with an iframe.
+  GURL main_url = embedded_test_server()->GetURL("example.com", "/title1.html");
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  RenderFrameHost* main_frame = shell()->web_contents()->GetPrimaryMainFrame();
+  GURL iframe_url =
+      embedded_test_server()->GetURL("example2.com", "/title1.html");
+
+  // Create the iframe.
+  ASSERT_TRUE(ExecJs(
+      main_frame,
+      base::StringPrintf("let iframe = document.createElement('iframe'); "
+                         "iframe.src = '%s'; "
+                         "document.body.appendChild(iframe);",
+                         iframe_url.spec().c_str())));
+  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+
+  // Get the RenderFrameHost for the iframe.
+  RenderFrameHost* iframe_host = nullptr;
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+  web_contents->ForEachRenderFrameHost([&](RenderFrameHost* rfh) {
+    if (rfh->GetLastCommittedURL() == iframe_url) {
+      iframe_host = rfh;
+    }
+  });
+  ASSERT_TRUE(iframe_host);
+
+  // Set up the media session service for the iframe.
+  MockMediaSessionServiceImpl mock_media_session_service(iframe_host);
+
+  // Set the metadata with artwork.
+  blink::mojom::SpecMediaMetadataPtr spec_metadata(
+      blink::mojom::SpecMediaMetadata::New());
+  spec_metadata->title = u"title";
+  spec_metadata->artist = u"artist";
+  spec_metadata->album = u"album";
+
+  std::vector<media_session::MediaImage> images;
+  media_session::MediaImage image;
+  image.src = favicon_server().GetURL("/artwork.png");
+  image.sizes.emplace_back(100, 100);
+  images.push_back(image);
+  spec_metadata->artwork = images;
+
+  mock_media_session_service.SetMetadata(std::move(spec_metadata));
+
+  // Start a player in the iframe.
+  auto player_observer = std::make_unique<MockMediaSessionPlayerObserver>(
+      iframe_host, media::MediaContentType::kPersistent);
+  StartNewPlayer(player_observer.get());
+  ResolveAudioFocusSuccess();
+
+  // Get the media image bitmap.
+  base::RunLoop run_loop;
+  media_session_->GetMediaImageBitmap(
+      images[0], 100, 100,
+      base::BindLambdaForTesting([&](const SkBitmap&) { run_loop.Quit(); }));
+  run_loop.Run();
+
+  // Check that the artwork was downloaded.
+  EXPECT_EQ(last_request_path_, "/artwork.png");
+
+  // Check that the artwork was downloaded from the iframe.
+  EXPECT_EQ(last_request_referer_, iframe_url.GetWithEmptyPath().spec());
+
+  RemovePlayers(player_observer.get());
+}
+
 class MediaSessionImplPrerenderingBrowserTest
     : public MediaSessionImplBrowserTest {
  public:
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/media/session/media_session_impl_browsertest.cc b/content/browser/media/session/media_session_impl_browsertest.cc
index b05a000..4655fe39 100644
--- a/content/browser/media/session/media_session_impl_browsertest.cc
+++ b/content/browser/media/session/media_session_impl_browsertest.cc
@@ -17,6 +17,7 @@
 #include "base/run_loop.h"
 #include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
@@ -384,10 +385,21 @@
  protected:
   std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
       const net::test_server::HttpRequest& request) {
+    last_request_path_ = request.relative_url;
+    auto it = request.headers.find("Referer");
+    if (it != request.headers.end()) {
+      last_request_referer_ = it->second;
+    } else {
+      last_request_referer_.clear();
+    }
+
     get_favicon_calls();
     return std::make_unique<net::test_server::BasicHttpResponse>();
   }
 
+  std::string last_request_path_;
+  std::string last_request_referer_;
+
   raw_ptr<MediaSessionImpl> media_session_ = nullptr;
   raw_ptr<MockAudioFocusDelegate> mock_audio_focus_delegate_ = nullptr;
   std::unique_ptr<MockMediaSessionServiceImpl> mock_media_session_service_;
@@ -3305,6 +3317,77 @@
 }
 #endif  // !BUILDFLAG(IS_ANDROID)
 
+IN_PROC_BROWSER_TEST_F(MediaSessionImplBrowserTest,
+                       DownloadArtworkFromCorrectFrame) {
+  // Navigate to a page with an iframe.
+  GURL main_url = embedded_test_server()->GetURL("example.com", "/title1.html");
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  RenderFrameHost* main_frame = shell()->web_contents()->GetPrimaryMainFrame();
+  GURL iframe_url =
+      embedded_test_server()->GetURL("example2.com", "/title1.html");
+
+  // Create the iframe.
+  ASSERT_TRUE(ExecJs(
+      main_frame,
+      base::StringPrintf("let iframe = document.createElement('iframe'); "
+                         "iframe.src = '%s'; "
+                         "document.body.appendChild(iframe);",
+                         iframe_url.spec().c_str())));
+  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+
+  // Get the RenderFrameHost for the iframe.
+  RenderFrameHost* iframe_host = nullptr;
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+  web_contents->ForEachRenderFrameHost([&](RenderFrameHost* rfh) {
+    if (rfh->GetLastCommittedURL() == iframe_url) {
+      iframe_host = rfh;
+    }
+  });
+  ASSERT_TRUE(iframe_host);
+
+  // Set up the media session service for the iframe.
+  MockMediaSessionServiceImpl mock_media_session_service(iframe_host);
+
+  // Set the metadata with artwork.
+  blink::mojom::SpecMediaMetadataPtr spec_metadata(
+      blink::mojom::SpecMediaMetadata::New());
+  spec_metadata->title = u"title";
+  spec_metadata->artist = u"artist";
+  spec_metadata->album = u"album";
+
+  std::vector<media_session::MediaImage> images;
+  media_session::MediaImage image;
+  image.src = favicon_server().GetURL("/artwork.png");
+  image.sizes.emplace_back(100, 100);
+  images.push_back(image);
+  spec_metadata->artwork = images;
+
+  mock_media_session_service.SetMetadata(std::move(spec_metadata));
+
+  // Start a player in the iframe.
+  auto player_observer = std::make_unique<MockMediaSessionPlayerObserver>(
+      iframe_host, media::MediaContentType::kPersistent);
+  StartNewPlayer(player_observer.get());
+  ResolveAudioFocusSuccess();
+
+  // Get the media image bitmap.
+  base::RunLoop run_loop;
+  media_session_->GetMediaImageBitmap(
+      images[0], 100, 100,
+      base::BindLambdaForTesting([&](const SkBitmap&) { run_loop.Quit(); }));
+  run_loop.Run();
+
+  // Check that the artwork was downloaded.
+  EXPECT_EQ(last_request_path_, "/artwork.png");
+
+  // Check that the artwork was downloaded from the iframe.
+  EXPECT_EQ(last_request_referer_, iframe_url.GetWithEmptyPath().spec());
+
+  RemovePlayers(player_observer.get());
+}
+
 class MediaSessionImplPrerenderingBrowserTest
     : public MediaSessionImplBrowserTest {
  public:
Loading diff…

Original Bug Report

reported by [email protected]

SameSite cookie bypass via Media Session artwork fetching

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.

Overview: A cross-origin subframe can trigger credentialed GET requests from the top-level frame by specifying Media Session artwork URLs. This occurs because the browser delegates the image download to the primary main frame instead of the frame that provided the metadata. The resulting request bypasses SameSite cookie restrictions and Fetch Metadata defenses, enabling potential GET-based CSRF attacks.

Affected files:

  • content/browser/media/session/media_session_impl.cc
  • content/browser/web_contents/web_contents_impl.cc
  • content/browser/web_contents/web_contents_android.cc
  • components/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaImageManager.java
  • third_party/blink/renderer/modules/image_downloader/multi_resolution_image_resource_fetcher.cc

Estimated timestamp from git blame: 2022-08-11

Description

A potential vulnerability exists in how Chrome handles MediaSession artwork fetching. A cross-origin subframe that plays media can become the routed media session and specify an artwork URL. When the browser fetches this artwork, it mistakenly delegates the download task to the top-level main frame rather than the subframe. This causes the network request to be sent with the main frame’s SiteForCookies and origin context, effectively bypassing SameSite cookie protections and Fetch Metadata (Sec-Fetch-Site) boundaries. This can be exploited to perform blind, GET-based Cross-Site Request Forgery (CSRF) against the top-level origin.

Technical Details

  1. When a cross-origin subframe plays media, it can be selected as the routed frame for the WebContents via MediaSessionImpl::ComputeFrameForRouting.
  2. The subframe can set navigator.mediaSession.metadata with an artwork URL pointing to a sensitive GET endpoint on the main frame’s origin.
  3. The browser receives this metadata via MediaSessionServiceImpl::SetMetadata. URL validation in MediaMetadataSanitizer::CheckSanity restricts the scheme (e.g., HTTP/HTTPS) but does not restrict the origin.
  4. When a UI observer (like Global Media Controls) needs to display the artwork, it triggers MediaSessionImpl::GetMediaImageBitmap (content/browser/media/session/media_session_impl.cc:1382).
  5. GetMediaImageBitmap calls web_contents()->DownloadImage(), which does not specify an initiator frame.
  6. In content/browser/web_contents/web_contents_impl.cc, DownloadImage delegates to DownloadImageInFrame using a default-constructed, empty GlobalRenderFrameHostId.
  7. Because the ID is empty, DownloadImageInFrame defaults to using GetPrimaryMainFrame() as the initiator frame.
  8. The Mojo IPC to download the image is sent to the top-level main frame’s renderer.
  9. The main frame renderer uses MultiResolutionImageResourceFetcher to make the request, setting the SiteForCookies to the main frame’s document (third_party/blink/renderer/modules/image_downloader/multi_resolution_image_resource_fetcher.cc:211).
  10. The Network Service treats the request as a same-site request, attaching SameSite=Strict and SameSite=Lax cookies, and applying a Sec-Fetch-Site: same-origin header. While the attacker cannot read the response (due to CORB/ORB and image decoding failures), state-changing GET endpoints will execute.

Potential Steps to Reproduce

Note: These are suggested steps based on static analysis, as our tooling agent cannot execute code to provide a working PoC.

  1. An attacker embeds an iframe (https://attacker.com/frame.html) on a victim site (https://victim.com).
  2. The user interacts with the iframe (or autoplay is permitted), causing it to play an audible media clip.
  3. The iframe executes JavaScript to set the media session artwork to a sensitive endpoint on the victim site:
    navigator.mediaSession.metadata = new MediaMetadata({
      title: 'Audio',
      artwork: [{ 
        src: 'https://victim.com/account/delete?confirm=true', 
        sizes: '512x512', 
        type: 'image/png' 
      }]
    });
    
  4. The browser’s media UI observes the metadata change and requests the artwork.
  5. The browser commands the victim.com main frame to fetch the URL.
  6. The GET request is sent to https://victim.com/account/delete?confirm=true with the user’s victim.com cookies and Sec-Fetch-Site: same-origin, executing the action.

Suggested Fix

Modify MediaSessionImpl::GetMediaImageBitmap to avoid using the WebContents::DownloadImage overload that lacks a frame context. Instead, it should track which RenderFrameHost (via GlobalRenderFrameHostId) provided the artwork URL. It should then call WebContents::DownloadImageInFrame, passing the correct GlobalRenderFrameHostId of the routed subframe so that the image fetch originates from the correct renderer process and security context.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker