CVE-2024-40794
Overview
Impact:
Description: Private Browsing tabs may be accessed without authentication
Researchers: Matthew Butler
Key insight. The root cause is a missing session-type policy check on a data path that crosses from a private (ephemeral) browsing context into persistent, system-visible UI; the fix is to gate Now Playing eligibility and metadata population on usesEphemeralSession() and mediaPlaybackIsSuspended() so private-session content never populates shared surfaces.
Patch Details
diff --git a/Source/WebCore/Modules/webaudio/AudioContext.cpp b/Source/WebCore/Modules/webaudio/AudioContext.cpp
index 4b55ff6f6e19..0f9a1164b0dc 100644
--- a/Source/WebCore/Modules/webaudio/AudioContext.cpp
+++ b/Source/WebCore/Modules/webaudio/AudioContext.cpp
@@ -524,6 +524,13 @@ bool AudioContext::isNowPlayingEligible() const
return false;
RefPtr document = this->document();
+ if (!document)
+ return false;
+
+ RefPtr page = document->page();
+ if (page && page->mediaPlaybackIsSuspended())
+ return false;
+
return hasPlayBackAudioSession(document.get());
}
@@ -553,6 +560,9 @@ std::optional<NowPlayingInfo> AudioContext::nowPlayingInfo() const
{ }
};
+ if (page->usesEphemeralSession())
+ return nowPlayingInfo;
+
#if ENABLE(MEDIA_SESSION)
if (RefPtr mediaSession = NavigatorMediaSession::mediaSessionIfExists(Ref { window->navigator() }))
mediaSession->updateNowPlayingInfo(nowPlayingInfo);
diff --git a/Source/WebCore/html/HTMLMediaElement.cpp b/Source/WebCore/html/HTMLMediaElement.cpp
index 12493772c729..75f0af913d5d 100644
--- a/Source/WebCore/html/HTMLMediaElement.cpp
+++ b/Source/WebCore/html/HTMLMediaElement.cpp
@@ -655,6 +655,15 @@ RefPtr<HTMLMediaElement> HTMLMediaElement::bestMediaElementForRemoteControls(Med
return selectedSession ? RefPtr { &downcast<MediaElementSession>(selectedSession.get())->element() } : nullptr;
}
+bool HTMLMediaElement::isNowPlayingEligible() const
+{
+ RefPtr page = document().page();
+ if (page && page->mediaPlaybackIsSuspended())
+ return false;
+
+ return m_mediaSession->hasNowPlayingInfo();
+}
+
std::optional<NowPlayingInfo> HTMLMediaElement::nowPlayingInfo() const
{
return m_mediaSession->computeNowPlayingInfo();
diff --git a/Source/WebCore/html/HTMLMediaElement.h b/Source/WebCore/html/HTMLMediaElement.h
index e4e90022ef85..2829f85687a2 100644
--- a/Source/WebCore/html/HTMLMediaElement.h
+++ b/Source/WebCore/html/HTMLMediaElement.h
@@ -960,7 +960,7 @@ private:
bool hasMediaStreamSource() const final;
void processIsSuspendedChanged() final;
bool shouldOverridePauseDuringRouteChange() const final;
- bool isNowPlayingEligible() const final { return m_mediaSession->hasNowPlayingInfo(); }
+ bool isNowPlayingEligible() const final;
std::optional<NowPlayingInfo> nowPlayingInfo() const final;
WeakPtr<PlatformMediaSession> selectBestMediaSession(const Vector<WeakPtr<PlatformMediaSession>>&, PlatformMediaSession::PlaybackControlsPurpose) final;
diff --git a/Source/WebCore/html/MediaElementSession.cpp b/Source/WebCore/html/MediaElementSession.cpp
index 1fe121b832f6..b6854f036586 100644
--- a/Source/WebCore/html/MediaElementSession.cpp
+++ b/Source/WebCore/html/MediaElementSession.cpp
@@ -1286,6 +1286,8 @@ std::optional<NowPlayingInfo> MediaElementSession::computeNowPlayingInfo() const
return { };
RefPtr page = m_element.document().page();
+ if (!page)
+ return { };
bool allowsNowPlayingControlsVisibility = page && !page->isVisibleAndActive();
bool isPlaying = state() == PlatformMediaSession::State::Playing;
@@ -1306,6 +1308,15 @@ std::optional<NowPlayingInfo> MediaElementSession::computeNowPlayingInfo() const
#endif
NowPlayingInfo info { m_element.mediaSessionTitle(), emptyString(), emptyString(), sourceApplicationIdentifier, duration, currentTime, rate, supportsSeeking, m_element.mediaUniqueIdentifier(), isPlaying, allowsNowPlayingControlsVisibility, { } };
+
+ if (page->usesEphemeralSession()) {
+ info.title = { };
+ info.artist = { };
+ info.album = { };
+ info.artwork = std::nullopt;
+ return info;
+ }
+
#if ENABLE(MEDIA_SESSION)
if (RefPtr session = mediaSession())
session->updateNowPlayingInfo(info);
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2024-40794 |
| Bugzilla | 275272 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Commit | 7bd7a66724608f88… |
| Advisory | Apple Advisory |
Terminology
- Now Playing info — A metadata record (title, artist, album, artwork, playback state) WebKit publishes to the operating system so currently playing media appears in Control Center, the lock screen, and remote-control surfaces.
- Ephemeral session — The non-persistent session type used by Private Browsing, where cookies, storage, and other state are kept in memory and are meant to leave no persistent, externally visible trace.
- MediaElementSession — The WebCore object that mediates between an HTMLMediaElement and platform media services, computing eligibility and the Now Playing payload for the element.
- mediaPlaybackIsSuspended — A per-page flag indicating the page’s media playback is currently suspended, during which media metadata should not be advertised to the system.
Root Cause Analysis
The bug is an information-disclosure logic error in WebKit’s Now Playing integration. When a media element or Web Audio context plays audio, WebCore computes a NowPlayingInfo structure (title, artist, album, artwork, unique identifier) and hands it to the platform so the media can be surfaced in system UI such as Control Center, the lock screen, and the Now Playing widget. The invariant that was violated is that content playing inside a private-browsing (ephemeral) session must never leak identifying metadata to persistent, system-visible surfaces, and that metadata must not be published while media playback for the page is suspended. Before the patch, HTMLMediaElement::isNowPlayingEligible() unconditionally returned m_mediaSession->hasNowPlayingInfo() and MediaElementSession::computeNowPlayingInfo() / AudioContext::nowPlayingInfo() built and returned the full info record without consulting the page’s session type, so a private tab’s media title (which can reveal the visited site or content) was exposed through the system Now Playing interface. The fix adds explicit ephemeral-session and suspension gates: AudioContext::isNowPlayingEligible() now returns false when there is no document or when page->mediaPlaybackIsSuspended(); AudioContext::nowPlayingInfo() returns an empty/placeholder record early when page->usesEphemeralSession(); HTMLMediaElement gains an out-of-line isNowPlayingEligible() that also returns false when playback is suspended; and MediaElementSession::computeNowPlayingInfo() null-checks the page and, for an ephemeral session, blanks title/artist/album/artwork before returning. These changes restore the invariant that ephemeral browsing produces no persistent, cross-context-visible Now Playing metadata. This is purely a policy/logic fix; nothing in the diff touches memory management, so there is no memory-corruption primitive here — the harm is confined to leaking private-session information to the local system UI.
Attack Path
1. Open media in a private tab
A user opens a Private Browsing tab and navigates to a page (attacker-controlled or benign) that plays audio via an <audio>/<video> element or a Web Audio AudioContext.
2. Metadata is computed
WebCore’s media session builds NowPlayingInfo from the element’s mediaSessionTitle() and related fields without checking that the owning page uses an ephemeral session.
3. Metadata is published to system UI
Because isNowPlayingEligible() returns true and computeNowPlayingInfo()/nowPlayingInfo() return the populated record, the private-tab title/artist/artwork are pushed to Control Center, the lock screen, and the Now Playing widget where they persist and are visible outside the private session.
4. Observation without authentication
Anyone with physical or shoulder-surfing access to the device (including at the lock screen, which requires no unlock) can read the leaked title/URL-derived metadata, effectively revealing private-browsing activity that was supposed to be unauthenticated-inaccessible.
Impact Assessment
This is an information-disclosure/privacy bug, not a memory-safety primitive: the diff contains no allocation, lifetime, or bounds logic that could be turned toward OOB/UAF, and there is no path to memory corruption or code execution from it. The concrete harm is that private-browsing media metadata (a title that can reveal the visited site or content) leaks to persistent, system-level Now Playing surfaces that are readable without authenticating the device (e.g. at the lock screen). It executes in and leaks from the WebContent process’s media pipeline into the platform’s shared Now Playing state; severity is medium precisely because it is a confidentiality leak rather than an exploitation chain.
Audit Directions
Other Now Playing / media-session outputs
Audit every producer of NowPlayingInfo and every isNowPlayingEligible()/computeNowPlayingInfo()/nowPlayingInfo() override (grep for NowPlayingInfo, hasNowPlayingInfo, updateNowPlayingInfo) to confirm each consults usesEphemeralSession() before emitting title/artist/artwork.
Ephemeral-session leaks in other platform bridges
Grep for usesEphemeralSession() and, conversely, for places that push page-derived strings to the OS (media remote, MediaSession API, spotlight/handoff, notifications) that do NOT check it — those are candidate private-mode leaks with the same shape.
Suspended-playback gaps
Grep for mediaPlaybackIsSuspended and confirm all metadata/remote-control code paths honor it; look for sibling PlatformMediaSession subclasses whose eligibility still ignores suspension.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
AudioContext::isNowPlayingEligible |
Source/WebCore/Modules/webaudio/AudioContext.cpp |
modified | Adds a null-document guard and returns false when the page’s media playback is suspended, before falling through to hasPlayBackAudioSession(). |
AudioContext::nowPlayingInfo |
Source/WebCore/Modules/webaudio/AudioContext.cpp |
modified | Returns the placeholder nowPlayingInfo early when page->usesEphemeralSession(), so private-session Web Audio never populates media-session metadata. |
HTMLMediaElement::isNowPlayingEligible |
Source/WebCore/html/HTMLMediaElement.cpp |
added | New out-of-line definition (previously an inline one-liner) that returns false when page->mediaPlaybackIsSuspended(), otherwise defers to m_mediaSession->hasNowPlayingInfo(). |
MediaElementSession::computeNowPlayingInfo |
Source/WebCore/html/MediaElementSession.cpp |
modified | Adds a null-page early return and, for an ephemeral session, blanks title/artist/album/artwork before returning so no identifying metadata escapes a private tab. |
HTMLMediaElement::isNowPlayingEligible (declaration) |
Source/WebCore/html/HTMLMediaElement.h |
modified | Changes the inline { return m_mediaSession->hasNowPlayingInfo(); } to an out-of-line declaration so the suspension check can be added. |