CVE-2026-28871
Overview
Background
- MediaDocument
- A synthetic WebCore document that wraps a media resource URL in a media element and never parses the payload as HTML, so any markup/script in the resource is inert.
- MediaPlayer::supportsType
- A runtime query that asks the available media engines whether they can play a given content type, returning IsNotSupported / MayBeSupported / IsSupported and depending on installed codecs and platform state.
- MIMETypeRegistry::isSupportedMediaMIMEType
- A static, table-driven predicate that reports whether a MIME type is a known/supported media type, used across WebKit to classify resources independently of live engine state.
- AVStreamDataParserMIMETypeCache
- A cache of MIME types the platform AVStreamDataParser (Media Source Extensions backend) reports it can parse, a dynamic and OS-version-dependent list.
- Cross-site scripting (XSS)
- Execution of attacker-controlled script in the security context (origin) of a target document, here achieved by getting HTML+script rendered as active content instead of an inert media document.
Root Cause Analysis
DOMImplementation::createDocument() decides what kind of Document to instantiate for a given content type. For media types it previously called the live media-engine query MediaPlayer::supportsType(parameters) and, if the result was anything other than IsNotSupported, created a MediaDocument (a synthetic document that merely wraps the URL in a <video> element and never parses the payload as markup).
The bug is that this check was NOT the same check the rest of WebKit uses to classify a resource as ‘media’: other code paths rely on MIMETypeRegistry::isSupportedMediaMIMEType(). Because the two predicates could disagree for a given MIME type, a resource could be classified one way for loading/handling and the opposite way for document creation. When a type that the registry considered ‘media’ (so the response was allowed through as media rather than sniffed/handled as HTML) fell through createDocument()’s divergent MediaPlayer::supportsType() test as IsNotSupported, WebKit did not build a safe MediaDocument and instead fell through to building a normal document that parses and executes the payload’s HTML and scripts. The bundled LayoutTest demonstrates the effect precisely: an iframe is pointed at ‘data:video/mp2t,<h1>Error</h1><script>parent.postMessage(“fail”,"*")</script>’, and the test only passes if that script does NOT run (no postMessage) while the document still reaches readyState ‘complete’ — i.e. the payload must be handled as an inert MediaDocument, not executed.
The fix restores a single, consistent classification: createDocument() now calls MIMETypeRegistry::isSupportedMediaMIMEType(contentType), the same predicate used elsewhere, so anything treated as media is uniformly turned into a MediaDocument.
The patch additionally hardens isSupportedMediaMIMEType() to reject any type whose lowercased form does not start with ‘video/’, ‘audio/’, or ‘application/’ before consulting the supported set, eliminating non-media prefixes from ever being accepted as media. Finally, MediaPlayerPrivateMediaSourceAVFObjC::getSupportedTypes() is changed to clear the set instead of returning AVStreamDataParserMIMETypeCache’s dynamically reported types; that dynamic, OS-dependent list was a source of divergence between MediaPlayer::supportsType() and the static registry, so removing it narrows the set of types whose media-vs-HTML disposition could differ across code paths. The violated invariant is ‘a resource classified as a supported media type is always rendered as an inert MediaDocument, never parsed as active content’; the fix re-establishes it by using one authoritative predicate and by shrinking the fuzzy, engine-dependent portion of the supported-type universe.
Attack Path
- Choose a divergent media MIME type Identify a MIME type (e.g. video/mp2t as used by the test) that MIMETypeRegistry treats as a supported media type — so the response is admitted as media and not sniffed as HTML — but for which the pre-patch MediaPlayer::supportsType() engine query returns IsNotSupported.
- Serve HTML+script under that type Deliver a payload whose bytes are HTML containing an inline <script>, but label it with the chosen media MIME type. The LayoutTest uses a data: URL (‘data:video/mp2t,<h1>Error</h1><script>…</script>’); a server response with Content-Type: video/mp2t works equivalently.
- Load it as a document Cause the payload to be loaded as a document — e.g. in an iframe, a navigation, or a subframe the attacker controls the response for — so DOMImplementation::createDocument() runs for that content type.
- Divergent check misfires createDocument()’s MediaPlayer::supportsType() returns IsNotSupported for the type, so it skips MediaDocument::create() and falls through to building a document that parses the payload as markup.
- Script executes The inline script runs in the resulting document’s context; the test detects this via a postMessage to the parent. In a real attack this yields script execution under whatever origin the response was served from, enabling cross-site scripting (e.g. against a site that lets users host files it believes will be treated as inert media).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
DOMImplementation::createDocumentSource/WebCore/dom/DOMImplementation.cpp |
modified | Replaced the MediaEngineSupportParameters + MediaPlayer::supportsType() query with a single MIMETypeRegistry::isSupportedMediaMIMEType(contentType) call to decide whether to build a MediaDocument, aligning document creation with the rest of WebKit's media-type classification. |
MIMETypeRegistry::isSupportedMediaMIMETypeSource/WebCore/platform/MIMETypeRegistry.cpp |
modified | Added a guard that lowercases the type and returns false unless it starts with 'video/', 'audio/', or 'application/', before checking supportedMediaMIMETypes(), preventing non-media prefixes from being accepted as media. |
MediaPlayerPrivateMediaSourceAVFObjC::getSupportedTypesSource/WebCore/platform/graphics/avfoundation/objc/MediaPlayerPrivateMediaSourceAVFObjC.mm |
modified | Now clears the output set instead of returning AVStreamDataParserMIMETypeCache::singleton().supportedTypes(), removing the dynamic OS-dependent type list that was a source of divergent media-type support answers. |
Files Changed
LayoutTests/media/iframe-load-html-as-m2ts-expected.txtLayoutTests/media/iframe-load-html-as-m2ts.htmlSource/WebCore/dom/DOMImplementation.cppSource/WebCore/platform/MIMETypeRegistry.cppSource/WebCore/platform/graphics/avfoundation/objc/MediaPlayerPrivateMediaSourceAVFObjC.mm
Audit Directions
- Other createDocument-style media decisionsGrep for remaining callers of MediaPlayer::supportsType and MediaEngineSupportParameters in document/loader factory paths (DOMImplementation.cpp, DocumentLoader, FrameLoader, PluginDocument) and check whether the media/plugin-vs-HTML decision there uses the same authoritative predicate now used in createDocument.
- Predicate mismatches across the MIME registryAudit every use of MIMETypeRegistry::isSupportedMediaMIMEType, isSupportedImageMIMEType, and the plugin/PDF equivalents for places where one code path decides ’this resource is X’ and another decides how to render it, looking for asymmetry that lets HTML be admitted under a non-HTML type.
- Dynamically populated supported-type setsSearch for getSupportedTypes implementations that copy from OS/engine caches (AVStreamDataParserMIMETypeCache, AVAssetMIMETypeCache, and similar ‘singleton().supportedTypes()’ patterns) and verify they cannot inject types with non-media prefixes or types that other classifiers do not also recognize.
- Prefix/normalization assumptions on MIME typesLook for MIME-type comparisons that do not normalize case or do not constrain the top-level type (missing convertToASCIILowercase() or missing startsWith(“video/”/“audio/”) checks) in supportedX MIMEType functions, since the fix shows these were exploitable gaps.