jQuery 3.1.1 violation of CSP directive

It looks like a bug or quirk of jQuery how it appends inline scripts ends up discarding all of their attributes and I can't see an obvious way of fixing it to test it I used the following HTML:

<!DOCTYPE html>
<html>

    <head>
        <meta http-equiv="Content-Security-Policy" content="default-src http://localhost 'nonce-123456' ; child-src 'none'; object-src 'none'; script-src 'nonce-123456';">
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.js" nonce="123456"></script> <!-- HTML nonce works -->
        <script nonce="123456">
            // This works
            console.log('Inline nonce works');

            // This will also work
            var s = document.createElement('script');
            s.setAttribute('nonce', '123456');
            s.textContent = 'console.log("Dynamically generated inline tag works")';
            document.head.appendChild(s);

            // This won't work
            var s2 = document.createElement('script');
            s2.setAttribute('nonce', '123456');
            s2.textContent = 'console.log("Dynamically generated inline tag appended via jQuery doesn\'t work")';
            $(document.head).append(s2); // This will throw a CSP error
        </script>
    </head>

<body>

</body>

</html>

When using jQuery to append it goes through the following process (reduced a little):

  • Creates a document fragment and appends a script tag to it
  • Applies a type="false/" attribute to the script tag
  • Removes the type attribute
  • If a src attribute is present it retrieves the script via Ajax (didn't investigate this further)
  • If not it runs DOMEval(node.textContent.replace(rcleanScript, ""), doc)

DomEval looks like this (with added comments):

doc = doc || document;
var script = doc.createElement( "script" );
script.textContent = code;
doc.head.appendChild( script ).parentNode.removeChild( script );

As you can see, no attributes would carry over to the new element before it was appended and as such CSP fails.

The solution would be to just use native JavaScript to append the element as opposed to jQuery or possibly wait on a bug fix/response to your report. I'm unsure what their reasoning would be to exclude attributes in this manner for inline script tags maybe a security feature?

The following should achieve what you want without jQuery - just set the textContent attribute to your JavaScript source code.

var script = document.createElement('script');
script.setAttribute('nonce', '<%=nonce%>');
script.textContent = '// Code here';
document.head.appendChild(script);

So essentially why that particular line throws the error is that the appended tag is actually a new tag with the same code and no attributes applied to it and as it has no nonce it's rejected by CSP.

Update: I've patched jQuery to fix this issue (is 3.1.2-pre patched but passing all tests), if you used my last fix I recommend updating to this version!

Minified: http://pastebin.com/gcLexN7z

Un-minified: http://pastebin.com/AEvzir4H

The branch is available here: https://github.com/Brian-Aykut/jquery/tree/3541-csp

Issue link: https://github.com/jquery/jquery/issues/3541

Changes in the code:

Line ~76 replace DOMEval function with:

function DOMEval( code, doc, attr ) {
    doc = doc || document;
    attr = attr || {};
    var script = doc.createElement( "script" );
    for ( var key in attr ) {
        if ( attr.hasOwnProperty( key ) ) {
            script.setAttribute( key, attr[ key ] );
        }
    }
    script.text = code;
    doc.head.appendChild( script ).parentNode.removeChild( script );
}

Add attr to var statement on ~line 5717 to

var fragment, first, scripts, hasScripts, node, doc, attr,

Change else body near line 5790 to:

attr = {};
if ( node.hasAttribute && node.hasAttribute( "nonce" ) ) {
    attr.nonce = node.getAttribute( "nonce" );
}
DOMEval( node.textContent.replace( rcleanScript, "" ), doc, attr );

OK, you just want to include jQuery, so you need to redefined the function appendChild (to disable it) before the jQuery script and after delete your custom function.

<script>
    var oldAppend = document.head.appendChild
    document.head.appendChild = function(script){
        if(script && script.tagName=='SCRIPT'){
            document.createElement('fakeHead').appendChild(script)//because script need a parent in the line that create the error
            return script
        }
        return oldAppend.apply(this,arguments)
    }
</script>

<script nonce="<%=nonce%>" type="text/javascript" src="jquery.js"></script>

<script>
    document.head.appendChild = oldAppend
</script>

Try this. It will redefine the append function of jQuery:

var oldAppend = $.fn.append
$.fn.append = function($el){
    var dom = ($el instanceOf $) ? $el[0] : $el
    if(dom && dom.tagName=='SCRIPT'){
        this[0].appendChild(dom)
        return this
    }
    return oldAppend.apply(this,arguments)
}