Introduction
CSP should be enforced by the browser as long as you stay on the page, and not just while it is originally loaded or rendered. Everything else would make it quite toothless.
So the behaviour you are experiencing has nothing to do with when the script is loaded. Instead it is about what loads it. There are two issues.
Problem 1: strict-dynamic
I'll let Mozilla explain it:
The strict-dynamic directive specifies that the trust explicitly given to a script present in the markup, by accompanying it with a nonce or a hash, shall be propagated to all the scripts loaded by that root script.
So, when you trust a script by giving it a nonce, it can in turn load other scripts. That means that if you have a DOM XSS vulnerability in a script with a nonce, the CSP will not save you. The solution is to remove 'strict-dynamic'
(but that will off course be problematic if you rely on this feature).
Problem 2: unsafe-eval
To load the script, you use jQuery .html()
. Somewhere deep in the jQuery source code (line 343 to be exact) it makes a call to the good old eval()
.
So if you run the code
$(".test").html("<scr" + "ipt>alert('XSS');</scr" + "ipt>");
jQuery will for some reason (don't ask me why, I have tried and failed to follow the source code) run the following:
eval("alert('XSS');");
This means that the CSP no longer applies - after all you are not creating a new script tag, you are evaluating a string.
Here the solution is to remove 'unsafe-eval'
from the CSP, but that would break jQuery. The solution to that might be to upgrade to jQuery 3, that upon cursory inspection of the source seems to have fixed this.
Conclusion
This means that all code (using jQuery 2) on the form
x.html(unsafeFromURL);
can be vulnerable to XSS even if a CSP is set. I think this is unexpected to a lot of people (it was for me). But it serves as a reminder of another important point: You should not use CSP as your only line of defence against XSS.
Also do note that this is not an issue with how CSP works, but with how jQuery works.