Star Citizen EmbedVideo Extension Stored XSS through wikitext caused by usage of non-reserved data attributes
Description
The EmbedVideo Extension is a MediaWiki extension which adds a parser function called #ev and various parser tags for embedding video clips from various video sharing services. In versions 4.0.0 and prior, the EmbedVideo extension allows adding arbitrary attributes to an HTML element, allowing for stored XSS through wikitext. This issue has been patched via commit 4e075d3.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Stored XSS in MediaWiki EmbedVideo extension via arbitrary HTML attribute injection through wikitext, patched in commit 4e075d3.
Vulnerability
The EmbedVideo extension for MediaWiki allows embedding videos via parser functions and tags like `. In versions 4.0.0 and prior, the extension did not sanitize or restrict the data-iframeconfig` attribute, allowing users to inject arbitrary HTML attributes through wikitext. This enabled stored cross-site scripting (XSS) attacks, as malicious attributes could be processed by the extension's JavaScript code.
Exploitation
An attacker with the ability to edit wikitext can craft a malicious ` or similar tag with a crafted data-iframeconfig` attribute containing JavaScript event handlers or other payloads. When a viewer loads the page, the injected attribute is rendered as part of the HTML, allowing the script to execute in the context of the user's browser session. No special privileges beyond editing rights are required [1][3].
Impact
Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of any user viewing the malicious page. This can lead to session hijacking, defacement, or theft of sensitive information, depending on the wiki's configuration and user permissions.
Mitigation
The issue has been patched in commit 4e075d3 [2], which renames the custom data attribute from data-iframeconfig to data-mw-iframeconfig. This prevents attribute injection because the extension no longer blindly processes user-supplied data-* attributes; it now only uses the prefixed version. Users should update to a version containing this commit or apply the patch manually. No known workarounds are available.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
starcitizenwiki/embedvideoPackagist | <= 4.0.0 | — |
Affected products
2- Range: <=4.0.0
- StarCitizenWiki/mediawiki-extensions-EmbedVideov5Range: <= 4.0.0
Patches
14e075d3dc9a1Merge commit from fork
6 files changed · +15 −15
includes/EmbedService/EmbedHtmlFormatter.php+1 −1 modified@@ -101,7 +101,7 @@ public static function toHtml( AbstractEmbedService $service, array $config = [] ->get( 'EmbedVideoRequireConsent' ); if ( $consent === true ) { $iframeConfig = sprintf( - "data-iframeconfig='%s'", + "data-mw-iframeconfig='%s'", $service->getIframeConfig( $width, $height ) ); }
includes/EmbedVideo.php+1 −1 modified@@ -183,7 +183,7 @@ public static function parseEVL( Parser $parser, PPFrame $frame, array $args ): } $linkConfig = [ - 'data-iframeconfig' => $ev->service->getIframeConfig( $ev->args['width'], $ev->args['height'] ), + 'data-mw-iframeconfig' => $ev->service->getIframeConfig( $ev->args['width'], $ev->args['height'] ), 'data-service' => $ev->args['service'], 'data-player' => $ev->args['player'] ?? 'default', 'class' => 'embedvideo-evl vplink',
resources/ext.embedVideo.videolink.js+4 −4 modified@@ -7,7 +7,7 @@ const {makeIframe, fetchThumb} = require('./iframe.js'); e.preventDefault(); const player = evl.dataset?.player ?? 'default'; - const iframeConfig = JSON.parse(evl.dataset?.iframeconfig ?? '{}'); + const iframeConfig = JSON.parse(evl.dataset?.mwIframeconfig ?? '{}'); const playerContainer = document.querySelector(`.embedvideo.evlplayer-${player}`); const iframe = playerContainer.querySelector('iframe'); @@ -25,7 +25,7 @@ const {makeIframe, fetchThumb} = require('./iframe.js'); // No iframe exists, only when explicit consent is required const div = document.querySelector(`.embedvideo.evlplayer-${player}`); - if (div === null || evl.dataset?.iframeconfig === null) { + if (div === null || evl.dataset?.mwIframeconfig === null) { console.warn(`No player with id '${player}' found!.`); return; } @@ -35,7 +35,7 @@ const {makeIframe, fetchThumb} = require('./iframe.js'); const origService = div.dataset?.service; - div.dataset.iframeconfig = evl.dataset.iframeconfig; + div.dataset.mwIframeconfig = evl.dataset.mwIframeconfig; div.dataset.service = evl.dataset.service; const serviceMessage = mw.message('embedvideo-service-' + (evl.dataset?.service ?? 'youtube')).escaped(); @@ -46,7 +46,7 @@ const {makeIframe, fetchThumb} = require('./iframe.js'); if (evl.dataset?.privacyUrl !== null) { const link = document.createElement('a'); - link.href = evl.dataset.privacyUrl; + link.href = evl.dataset.privacyUrl; link.rel = 'nofollow,noopener'; link.target = '_blank'; link.classList.add('embedvideo-privacyNotice__link');
resources/modules/iframe.js+4 −4 modified@@ -82,7 +82,7 @@ const fetchThumb = async (url, parent, container) => { title.classList.add('embedvideo-loader__title'); link.classList.add('embedvideo-loader__link'); - link.href = JSON.parse(container?.dataset?.iframeconfig ?? '{"src": "#"}').src; + link.href = JSON.parse(container?.dataset?.mwIframeconfig ?? '{"src": "#"}').src; link.target = '_blank'; link.rel = 'noopener noreferrer nofollow'; link.innerText = json.title; @@ -126,7 +126,7 @@ const makeIframe = function(ev) { const wrapper = ev.querySelector('.embedvideo-wrapper'); const getIframeConfig = function() { - let iframeConfig = ev.dataset.iframeconfig; + let iframeConfig = ev.dataset.mwIframeconfig; iframeConfig = { ...mw.config.get('ev-' + ev.dataset.service + '-config') ?? [], @@ -176,7 +176,7 @@ const makeIframe = function(ev) { /** @type HTMLDivElement|null */ const consentDiv = wrapper.querySelector('.embedvideo-consent'); - let iframeConfig = ev.dataset.iframeconfig; + let iframeConfig = ev.dataset.mwIframeconfig; if (consentDiv === null || iframeConfig === null) { return; @@ -206,4 +206,4 @@ const makeIframe = function(ev) { } } -module.exports = { makeIframe, fetchThumb } \ No newline at end of file +module.exports = { makeIframe, fetchThumb }
tests/phpunit/EmbedService/EmbedHtmlFormatterTest.php+1 −1 modified@@ -44,7 +44,7 @@ public function testToHtmlConsent() { $service = EmbedServiceFactory::newFromName( 'archiveorg', 'foo' ); // phpcs:ignore Generic.Files.LineLength.TooLong - $this->assertStringContainsString( 'data-iframeconfig=\'{"src":"//archive.org/embed/foo"}\'', EmbedHtmlFormatter::toHtml( $service ) ); + $this->assertStringContainsString( 'data-mw-iframeconfig=\'{"src":"//archive.org/embed/foo"}\'', EmbedHtmlFormatter::toHtml( $service ) ); $this->assertStringNotContainsString( '<iframe', EmbedHtmlFormatter::toHtml( $service ) ); }
tests/phpunit/EmbedVideoTest.php+4 −4 modified@@ -136,7 +136,7 @@ public function testParseEVYouTubeValid() { $this->assertIsArray( $output ); $this->assertCount( 3, $output ); // phpcs:ignore Generic.Files.LineLength.TooLong - $this->assertStringContainsString( '<figure class="embedvideo" data-service="youtube" data-iframeconfig=\'{"src":"https://www.youtube-nocookie.com/embed/foobar?autoplay=1"}\' style="width:640px">', $output[0] ); + $this->assertStringContainsString( '<figure class="embedvideo" data-service="youtube" data-mw-iframeconfig=\'{"src":"https://www.youtube-nocookie.com/embed/foobar?autoplay=1"}\' style="width:640px">', $output[0] ); } /** @@ -308,7 +308,7 @@ public function testParseEVTagIdInInput() { $this->assertIsArray( $output ); $this->assertCount( 3, $output ); $this->assertStringContainsString( - '<figure class="embedvideo" data-service="youtube" data-iframeconfig', + '<figure class="embedvideo" data-service="youtube" data-mw-iframeconfig', $output[0] ); } @@ -339,7 +339,7 @@ public function testParseArgsExample1() { $this->assertIsArray( $output ); $this->assertCount( 3, $output ); $this->assertStringContainsString( - '<figure class="embedvideo" data-service="youtube" data-iframeconfig', + '<figure class="embedvideo" data-service="youtube" data-mw-iframeconfig', $output[0] ); } @@ -438,7 +438,7 @@ public function testParseEVLYouTube() { $this->assertIsArray( $output ); $this->assertCount( 3, $output ); // phpcs:ignore Generic.Files.LineLength.TooLong - $this->assertStringContainsString( '<a data-iframeconfig="', $output[0] ); + $this->assertStringContainsString( '<a data-mw-iframeconfig="', $output[0] ); $this->assertStringContainsString( 'Test Text', $output[0] ); }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-4j5h-mvj3-m48vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-59839ghsaADVISORY
- github.com/StarCitizenWiki/mediawiki-extensions-EmbedVideo/blob/440fb331a84b2050f4cc084c1d31d58a1d1c202d/resources/ext.embedVideo.videolink.jsghsax_refsource_MISCWEB
- github.com/StarCitizenWiki/mediawiki-extensions-EmbedVideo/blob/440fb331a84b2050f4cc084c1d31d58a1d1c202d/resources/modules/iframe.jsghsax_refsource_MISCWEB
- github.com/StarCitizenWiki/mediawiki-extensions-EmbedVideo/commit/4e075d3dc9a15a3ee53f449a684d5ab847e52f01ghsax_refsource_MISCWEB
- github.com/StarCitizenWiki/mediawiki-extensions-EmbedVideo/security/advisories/GHSA-4j5h-mvj3-m48vghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.