CVE-2024-47605
Description
silverstripe-asset-admin is a silverstripe assets gallery for asset management. When using the "insert media" functionality, the linked oEmbed JSON includes an HTML attribute which will replace the embed shortcode. The HTML is not sanitized before replacing the shortcode, allowing a script payload to be executed on both the CMS and the front-end of the website. This issue has been addressed in silverstripe/framework version 5.3.8 and users are advised to upgrade. There are no known workarounds for this vulnerability.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A stored XSS vulnerability in Silverstripe CMS allows script payloads to execute via unsanitized oEmbed HTML, affecting both CMS and frontend users.
Vulnerability
Overview CVE-2024-47605 is a stored cross-site scripting (XSS) vulnerability in the silverstripe-asset-admin module, which provides an asset gallery for Silverstripe CMS. When using the "insert media" functionality, the linked oEmbed JSON includes an HTML attribute that replaces the embed shortcode. The HTML is not sanitized before this replacement, allowing a script payload to be executed on both the CMS and the front-end of the website. This issue affects versions prior to the fix in silverstripe/framework 5.3.8 [1][2].
Exploitation and
Attack Surface An attacker with the ability to insert media content (e.g., a user with CMS access or an external source providing a malicious oEmbed response) can inject arbitrary HTML and JavaScript. The unsanitized HTML from the oEmbed response is directly embedded into the page, enabling the execution of script payloads. No authentication is required beyond the privileges needed to use the media insertion feature, and the attack vector is over the network [1][2].
Impact
Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of a victim's browser session. This can lead to session hijacking, defacement, data theft, or other malicious actions, as the script runs on both the CMS administrative interface and the public-facing website. The vulnerability has a CVSS v3.1 base score of 5.4 (Medium), reflecting its potential for significant harm within the affected scope [1][2].
Mitigation
The vulnerability is fixed in Silverstripe Framework version 5.3.8. Users must upgrade to this version or later to remediate the issue. The fix, contained in commit 09b5052, wraps embeds containing script or style tags in an iframe with a data:text/html;charset=utf-8 source, preventing direct execution of injected scripts. No official workarounds are available [1][2][3].
AI Insight generated on May 20, 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 |
|---|---|---|
silverstripe/frameworkPackagist | < 5.3.8 | 5.3.8 |
Affected products
42.2.0-rc1, 2.2.2-rc1, 2.3.0-rc1, …+ 1 more
- (no CPE)range: 2.2.0-rc1, 2.2.2-rc1, 2.3.0-rc1, …
- (no CPE)
Patches
28b8404e4781f09b5052c8693[CVE-2024-47605] Wrap embeds containing script or style tags in an iframe (#11554)
2 files changed · +308 −1
src/View/Shortcodes/EmbedShortcodeProvider.php+96 −0 modified@@ -6,6 +6,7 @@ use Embed\Http\RequestException; use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\InvalidArgumentException; +use RuntimeException; use SilverStripe\Core\Convert; use SilverStripe\Core\Injector\Injector; use SilverStripe\ORM\ArrayList; @@ -28,6 +29,31 @@ class EmbedShortcodeProvider implements ShortcodeHandler { use Configurable; + /** + * Domains to exclude from sandboxing content in an iframe + * This will also exclude any subdomains + * e.g. if 'example.com' is excluded then 'www.example.com' will also be excluded + * Do not include the protocol in the domain i.e. exclude the leading https:// + */ + private static array $domains_excluded_from_sandboxing = []; + + /** + * Attributes to add to the iframe when sandboxing + * Note that the 'src' attribute cannot be set via config + * If a style attribute is set via config, width and height values will be overriden by + * any shortcode width and height arguments + */ + private static array $sandboxed_iframe_attributes = []; + + /** + * The url of the last extractor used + * This is used instead of adding a new param to an existing method + * which would be backwards incompatible + * + * @internal + */ + private static string $extractorUrl = ''; + /** * Gets the list of shortcodes provided by this handler * @@ -140,6 +166,7 @@ public static function embeddableToHtml(Embeddable $embeddable, array $arguments return ''; } $extractor = $embeddable->getExtractor(); + EmbedShortcodeProvider::$extractorUrl = (string) $extractor->url; $type = $embeddable->getType(); if ($type === 'video' || $type === 'rich') { // Attempt to inherit width (but leave height auto) @@ -194,6 +221,7 @@ protected static function videoEmbed($arguments, $content) ])); } + $content = EmbedShortcodeProvider::sandboxHtml($content, $arguments); $data = [ 'Arguments' => $arguments, 'Attributes' => $attributes, @@ -342,4 +370,72 @@ private static function cleanKeySegment(string $str): string { return preg_replace('/[^a-zA-Z0-9\-]/', '', $str ?? ''); } + + /** + * Wrap potentially dangerous html embeds in an iframe to sandbox them + * Potentially dangerous html embeds would could be those that contain <script> or <style> tags + * or html that contains an on*= attribute + */ + private static function sandboxHtml(string $html, array $arguments) + { + // Do not sandbox if the domain is excluded + if (EmbedShortcodeProvider::domainIsExcludedFromSandboxing()) { + return $html; + } + // Do not sandbox if the html is already an iframe + if (preg_match('#^<iframe[^>]*>#', $html) && preg_match('#</iframe\s*>$#', $html)) { + // Prevent things like <iframe><script>alert(1)</script></iframe> + // and <iframe></iframe><unsafe stuff here/><iframe></iframe> + // If there's more than 2 HTML tags then sandbox it + if (substr_count($html, '<') <= 2) { + return $html; + } + } + // Sandbox the html in an iframe + $style = ''; + if (!empty($arguments['width'])) { + $style .= 'width:' . intval($arguments['width']) . 'px;'; + } + if (!empty($arguments['height'])) { + $style .= 'height:' . intval($arguments['height']) . 'px;'; + } + $attrs = array_merge([ + 'frameborder' => '0', + ], static::config()->get('sandboxed_iframe_attributes')); + $attrs['src'] = 'data:text/html;charset=utf-8,' . rawurlencode($html); + if (array_key_exists('style', $attrs)) { + $attrs['style'] .= ";$style"; + $attrs['style'] = ltrim($attrs['style'], ';'); + } else { + $attrs['style'] = $style; + } + $html = HTML::createTag('iframe', $attrs); + return $html; + } + + /** + * Check if the domain is excluded from sandboxing based on config + */ + private static function domainIsExcludedFromSandboxing(): bool + { + $domain = (string) parse_url(EmbedShortcodeProvider::$extractorUrl, PHP_URL_HOST); + $config = static::config()->get('domains_excluded_from_sandboxing'); + foreach ($config as $excluded) { + if (!is_string($excluded)) { + throw new RuntimeException('domains_excluded_from_sandboxing must be an array of strings'); + } + $excludedDomain = parse_url($excluded, PHP_URL_HOST); + if (!$excludedDomain) { + // Try adding a protocol so that parse_url can parse it + $excludedDomain = parse_url('http://' . $excluded, PHP_URL_HOST); + } + if (!$excludedDomain) { + throw new RuntimeException('Not a valid domain: ' . $excluded); + } + if (str_ends_with($domain, $excludedDomain)) { + return true; + } + } + return false; + } }
tests/php/View/Shortcodes/EmbedShortcodeProviderTest.php+212 −1 modified@@ -2,12 +2,16 @@ namespace SilverStripe\View\Tests\Shortcodes; +use Embed\Extractor; use Psr\SimpleCache\CacheInterface; use SilverStripe\Core\Config\Config; use SilverStripe\View\Parsers\ShortcodeParser; use SilverStripe\View\Shortcodes\EmbedShortcodeProvider; use SilverStripe\Dev\SapphireTest; use SilverStripe\View\Tests\Embed\EmbedUnitTest; +use SilverStripe\View\Embed\EmbedContainer; +use stdClass; +use RuntimeException; class EmbedShortcodeProviderTest extends EmbedUnitTest { @@ -126,7 +130,7 @@ public function testFlickr() ); $this->assertEqualIgnoringWhitespace( <<<EOT - <div style="width:1024px;"><a data-flickr-embed="true" href="https://www.flickr.com/photos/philocycler/32119532132/" title="birdbyPhilocycler,onFlickr"><img src="https://live.staticflickr.com/759/32119532132_50c3f7933f_b.jpg" width="1024" height="742" alt="bird"></a><script asyncsrc="https://embedr.flickr.com/assets/client-code.js" charset="utf-8"></script><p class="caption">Birdy</p></div> + <div style="width:1024px;"><iframe frameborder="0"src="data:text/html;charset=utf-8,%3Ca%20data-flickr-embed%3D%22true%22%20href%3D%22https%3A%2F%2Fwww.flickr.com%2Fphotos%2Fphilocycler%2F32119532132%2F%22%20title%3D%22bird%20by%20Philocycler%2C%20on%20Flickr%22%3E%3Cimg%20src%3D%22https%3A%2F%2Flive.staticflickr.com%2F759%2F32119532132_50c3f7933f_b.jpg%22%20width%3D%221024%22%20height%3D%22742%22%20alt%3D%22bird%22%3E%3C%2Fa%3E%3Cscript%20async%20src%3D%22https%3A%2F%2Fembedr.flickr.com%2Fassets%2Fclient-code.js%22%20charset%3D%22utf-8%22%3E%3C%2Fscript%3E" style="width:1024px;height:742px;"></iframe><p class="caption">Birdy</p></div> EOT, $html ); @@ -217,4 +221,211 @@ public function testOnlyWhitelistedAttributesAllowed() $html ); } + + public function provideSandboxHtml(): array + { + return [ + 'normal' => [ + 'url' => 'http://example.com/embed', + 'excluded' => [], + 'html' => 'Some content', + 'attrs' => [], + 'exception' => false, + 'expected' => '<divstyle="width:100px;"><iframe frameborder="0"src="data:text/html;' + . 'charset=utf-8,Some%20content"style="width:100px;"></iframe></div>', + ], + 'normal-with-attrs' => [ + 'url' => 'http://example.com/embed', + 'excluded' => [], + 'html' => 'Some content', + 'attrs' => [ + 'frameborder' => '1', + 'style' => 'width:200px;height:200px', + 'data-something' => 'lorem' + ], + 'exception' => false, + 'expected' => '<div style="width:100px;"><iframe frameborder="1"style="width:200px;' + . 'height:200px;width:100px;" data-something="lorem" src="data:text/html;charset=utf-8,' + . 'Some%20content"></iframe></div>', + ], + 'excluded' => [ + 'url' => 'http://example.com/embed', + 'excluded' => ['example.com'], + 'html' => 'Some content', + 'attrs' => [], + 'exception' => false, + 'expected' => '<div style="width:100px;">Some content</div>', + ], + 'subdomain-excluded' => [ + 'url' => 'http://sub.example.com/embed', + 'excluded' => ['example.com'], + 'html' => 'Some content', + 'attrs' => [], + 'exception' => false, + 'expected' => '<div style="width:100px;">Some content</div>', + ], + 'config-includes-protocol' => [ + 'url' => 'http://example.com/embed', + 'excluded' => ['http://example.com'], + 'html' => 'Some content', + 'attrs' => [], + 'exception' => false, + 'expected' => '<div style="width:100px;">Some content</div>', + ], + 'config-includes-wrong-protocol' => [ + 'url' => 'https://example.com/embed', + 'excluded' => ['http://example.com'], + 'html' => 'Some content', + 'attrs' => [], + 'exception' => false, + 'expected' => '<div style="width:100px;">Some content</div>', + ], + 'umatched-config' => [ + 'url' => 'https://example.com/embed', + 'excluded' => ['somewhere.com'], + 'html' => 'Some content', + 'attrs' => [], + 'exception' => false, + 'expected' => '<div style="width:100px;"><iframe frameborder="0" src="data:text/html;' + . 'charset=utf-8,Some%20content"style="width:100px;"></iframe></div>', + ], + 'invalid-config' => [ + 'url' => 'https://example.com/embed', + 'excluded' => [123], + 'html' => 'Some content', + 'attrs' => [], + 'exception' => true, + 'expected' => '', + ], + 'iframe' => [ + 'url' => 'http://example.com/embed', + 'excluded' => [], + 'html' => '<iframe src="https://example.com/content"></iframe>', + 'attrs' => [], + 'exception' => false, + 'expected' => '<div style="width:100px;"><iframe src="https://example.com/content"></iframe></div>', + ], + 'iframe-short' => [ + 'url' => 'http://example.com/embed', + 'excluded' => [], + 'html' => '<iframe src="https://example.com/content"/>', + 'attrs' => [], + 'exception' => false, + 'expected' => '<div style="width:100px;"><iframe frameborder="0" src="data:text/html;charset=utf-8,' + . '%3Ciframe%20src%3D%22https%3A%2F%2Fexample.com%2Fcontent%22%2F%3E" style="width:100px;">' + . '</iframe></div>', + ], + 'iframe-whitespace-in-tags' => [ + 'url' => 'http://example.com/embed', + 'excluded' => [], + 'html' => '<iframe src="https://example.com/content" ></iframe >', + 'attrs' => [], + 'exception' => false, + 'expected' => '<div style="width:100px;"><iframe src="https://example.com/content"></iframe></div>', + ], + 'iframe-with-content-inside' => [ + 'url' => 'http://example.com/embed', + 'excluded' => [], + 'html' => '<iframe><div>something</div></iframe>', + 'attrs' => [], + 'exception' => false, + 'expected' => '<divstyle="width:100px;"><iframe frameborder="0"src="data:text/html;charset=utf-8,' + . '%3Ciframe%3E%3Cdiv%3Esomething%3C%2Fdiv%3E%3C%2Fiframe%3E"style="width:100px;"></iframe></div>', + ], + 'closed-iframe' => [ + 'url' => 'http://example.com/embed', + 'excluded' => [], + 'html' => '</iframe>', + 'attrs' => [], + 'exception' => false, + 'expected' => '<div style="width:100px;"><iframe frameborder="0"src="data:text/html;' + . 'charset=utf-8,%3C%2Fiframe%3E"style="width:100px;"></iframe></div>', + ], + 'malicious-iframe-1' => [ + 'url' => 'https://example.com/embed', + 'excluded' => [], + 'html' => '<iframe></iframe>bad<iframe></iframe>', + 'attrs' => [], + 'exception' => false, + 'expected' => '<divstyle="width:100px;"><iframe frameborder="0"src="data:text/html;' + . 'charset=utf-8,%3Ciframe%3E%3C%2Fiframe%3Ebad%3Ciframe%3E%3C%2Fiframe%3E"' + . 'style="width:100px;"></iframe></div>', + ], + 'malicious-iframe-2' => [ + 'url' => 'https://example.com/embed', + 'excluded' => [], + 'html' => '<iframe src="http://example.com/thing"></iframe>bad<iframe src="http://example.com/thing"></iframe>', + 'attrs' => [], + 'exception' => false, + 'expected' => '<div style="width:100px;"><iframe frameborder="0"src="data:text/html;' + . 'charset=utf-8,%3Ciframe%20src%3D%22http%3A%2F%2Fexample.com%2Fthing%22%3E%3C%2F' + . 'iframe%3Ebad%3Ciframe%20src%3D%22http%3A%2F%2Fexample.com%2Fthing%22%3E%3C' + . '%2Fiframe%3E"style="width:100px;"></iframe></div>', + ], + ]; + } + + /** + * @dataProvider provideSandboxHtml + */ + public function testSandboxHtml( + string $url, + array $excluded, + string $html, + array $attrs, + bool $exception, + string $expected + ): void { + if ($exception) { + $this->expectException(RuntimeException::class); + } + $embeddable = $this->getEmbeddable($url, $html); + $attributes = ['width' => 100]; + EmbedShortcodeProvider::config()->set('domains_excluded_from_sandboxing', $excluded); + EmbedShortcodeProvider::config()->set('sandboxed_iframe_attributes', $attrs); + $actual = EmbedShortcodeProvider::embeddableToHtml($embeddable, $attributes); + if (!$exception) { + $this->assertEqualIgnoringWhitespace($expected, $actual); + } + } + + private function getEmbeddable(string $url, string $html) + { + return new class($url, $html) extends EmbedContainer { + private $_url; + private $_html; + public function __construct($url, $html) + { + $this->_url = $url; + $this->_html = $html; + parent::__construct($url); + } + public function getType() + { + return 'rich'; + } + public function getExtractor(): Extractor + { + return new class($this->_url, $this->_html) extends Extractor { + protected $_url; + private $_html; + public function __construct($url, $html) + { + $this->_url = $url; + $this->_html = $html; + } + public function __get($name) + { + $code = new stdClass; + $code->html = $this->_html; + return match ($name) { + 'code' => $code, + 'url' => $this->_url, + default => null, + }; + } + }; + } + }; + } }
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-7cmp-cgg8-4c82ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-47605ghsaADVISORY
- github.com/FriendsOfPHP/security-advisories/blob/master/silverstripe/framework/CVE-2024-47605.yamlghsaWEB
- github.com/silverstripe/silverstripe-asset-admin/security/advisories/GHSA-7cmp-cgg8-4c82nvdWEB
- github.com/silverstripe/silverstripe-framework/commit/09b5052c86932f273e0d733428c9aade70ff2a4anvdWEB
- www.silverstripe.org/download/security-releases/cve-2024-47605nvdWEB
News mentions
0No linked articles in our index yet.