Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jQuery 3.1.1 violation of CSP directive

I'm using jQuery version 3.1.1 and I'm trying to implement Content Security Policy (CSP) directives on my webpage.

I'm getting the following error:

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-c20t41c7-73c6-4bf9-fde8-24a7b35t5f71'". Either the 'unsafe-inline' keyword, a hash ('sha256-KAcpKskREkEQf5B3mhDTonpPg34XnzaUC5IoBrOUrwY='), or a nonce ('nonce-...') is required to enable inline execution.

The error is produced on line 82 of the main jquery.js script file. The content of this line is:

doc.head.appendChild( script ).parentNode.removeChild( script );

Basically, it adds an inline script tag to the DOM, that violates the CSP.

I do not want to use 'unsafe-inline'. Is there another way to circumvent this error?

As you can see on the CSP violation, I'm using CSP level 2 (nonce), but it is ignored. Would it be possible (somehow) to inform jQuery to use this nonce when appending the script tag?


This is how the HTML looks like (using an Express.js template for nonce)

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

Once the HTML is rendered, the nonce attribute on the script tag matches the CSP nonce directive sent by the server.


It does work with plain JavaScript:

<script nonce="<%=nonce%>" type="text/javascript">
        var userEmail = "<%=user.user.email%>";
</script>

Without the nonce attribute, this script tag would violate the CSP directive.

like image 833
Victor Gaspar Avatar asked Feb 25 '17 00:02

Victor Gaspar


2 Answers

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 );
like image 180
Brian Avatar answered Nov 03 '22 11:11

Brian


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)
}
like image 33
bormat Avatar answered Nov 03 '22 13:11

bormat