CVE-2026-11181
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/media/session/media_session_impl.cc |
modified | |
BindLambdaForTestingcontent/browser/media/session/media_session_impl_browsertest.cc |
modified | |
MediaSessionImplPrerenderingBrowserTestcontent/browser/media/session/media_session_impl_browsertest.cc |
modified |
Files Changed
content/browser/media/session/media_session_impl.cccontent/browser/media/session/media_session_impl_browsertest.cc
Patch
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:
Regression Test / PoC
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:
Original Bug Report
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.cccontent/browser/web_contents/web_contents_impl.cccontent/browser/web_contents/web_contents_android.cccomponents/browser_ui/media/android/java/src/org/chromium/components/browser_ui/media/MediaImageManager.javathird_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
- When a cross-origin subframe plays media, it can be selected as the routed frame for the
WebContentsviaMediaSessionImpl::ComputeFrameForRouting. - The subframe can set
navigator.mediaSession.metadatawith anartworkURL pointing to a sensitive GET endpoint on the main frame’s origin. - The browser receives this metadata via
MediaSessionServiceImpl::SetMetadata. URL validation inMediaMetadataSanitizer::CheckSanityrestricts the scheme (e.g., HTTP/HTTPS) but does not restrict the origin. - 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). GetMediaImageBitmapcallsweb_contents()->DownloadImage(), which does not specify an initiator frame.- In
content/browser/web_contents/web_contents_impl.cc,DownloadImagedelegates toDownloadImageInFrameusing a default-constructed, emptyGlobalRenderFrameHostId. - Because the ID is empty,
DownloadImageInFramedefaults to usingGetPrimaryMainFrame()as the initiator frame. - The Mojo IPC to download the image is sent to the top-level main frame’s renderer.
- The main frame renderer uses
MultiResolutionImageResourceFetcherto make the request, setting theSiteForCookiesto the main frame’s document (third_party/blink/renderer/modules/image_downloader/multi_resolution_image_resource_fetcher.cc:211). - The Network Service treats the request as a same-site request, attaching
SameSite=StrictandSameSite=Laxcookies, and applying aSec-Fetch-Site: same-originheader. 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.
- An attacker embeds an iframe (
https://attacker.com/frame.html) on a victim site (https://victim.com). - The user interacts with the iframe (or autoplay is permitted), causing it to play an audible media clip.
- 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' }] }); - The browser’s media UI observes the metadata change and requests the artwork.
- The browser commands the
victim.commain frame to fetch the URL. - The GET request is sent to
https://victim.com/account/delete?confirm=truewith the user’svictim.comcookies andSec-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.