First, let us define what an inline and external scripts are. An HTML page can include a script code with the code right inside the tags - this is an inline script
If the same tag has both
script is executed only.
The security risk
Inserting "evil" inline script tags into a page is a common attack. If you ever allow the user-produced content to be linked into the DOM (to control its appearance for example), then your website is vulnerable to this attack. If you allow the user to enter this text
this is a <strong>tag</strong> and this is <em>emphasized</em>
and then insert the text as HTML into your page, very little is stopping the user
from entering "bad" text like this one that have a
<script> tag inside
this is a <strong>tag</strong> and my script.
There are a lot of ways the
<script> tag can hide inside the HTML, escaping
(pun intended) even the most sophisticated text sanitizers.
You can see this in action by visiting the demo page.
Clicking the green button inserts the "good" styled content.
inside the inlined
<script> tag, opening an alert box.
The source in the script tag can now do anything it wants - like grab the sensitive data and send somewhere, or make API calls on the user's behalf. It is pretty severe compromise at this point.
<script> ... source ... </script> tags in the page.
The way to do this in the modern browsers is to set the 'Content-Security-Policy' (CSP)
property, either via
meta attribute or headers. Read these two 1, 2
references to learn about CSP.
See the notes at the end of this post regarding browser support
(summary: the way I use it in this blog post is widely supported by the modern browsers).
You can see a very strict and solid CSP header if you curl
$ curl -I https://github.com
It is a beast, and I had split the relevant header across multiple lines to clarity. I also trimmed long lists of domains in some lines.
The most important for our purpose is the limit the GitHub places on where the scripts
can be sourced: only from
assets-cdn.github.com domain. Thus the scripts that are
inlined in the page itself are NOT going to run. Try it now
- Open the browser
- Browser to
- Open the Developer Tools console
- Paste the following code into the console to create a new inline script
var el = document.createElement('script');
You can find this code snippet in this collection.
When you paste the code snippet, it tries to run the code right away, but you get an error in the console
Refused to execute inline script because it violates the following
Basically the script we tried to execute was stopped by the browser. The browser has checked and determined that the script did NOT
- come from the list of trusted sources
assets-cdn.github.com(we executed it directly from
- the inline script execution flag
script-src unsafe-inlineis not present int the CSP header.
- the page also allows specific white-listed scripts via SHA list, but our script obviously is not on this list.
Now do the same on
cnn.com, and it runs just fine.
So if there is user supplied content (like visitor comments), and it is not validated very well, it could potentially be linked directly into the DOM, executing the user supplied source, as if was valid code written by the CNN page developers.
For simplicity, we are going to set the security policy on our demo page
meta attribute instead of the recommended way (response header 'Content-Security-Policy').
This way we can still use the static web page without running a demo server.
We can place the following into our page
<meta http-equiv="Content-Security-Policy" content="script-src https://code.jquery.com 'self';">
https://code.jquery.com- this is were our jQuery comes from in the script
self- this are all the scripts linked as external files and hosted by the current domain, like the
When we try the same buttons, we can still get the styled HTML elements in the DOM (highlighted in red boxes). But the second button "Link bad input to DOM" fails to run the script (see the red arrow pointing at the error shown in the console)
You can try the page yourself here.
As a partial bonus, if you have a global error handler, like Sentry, Raygun, or your own, you can catch an error notification (but without any details, at least in Chrome 46) when a script tries to run but is prevented.
There are two levels of 'Content-Security-Policy' standards. Then one I have shown here, if used via a response header is widely supported. Most of the modern browsers (IE10+) support setting the list of allowed script sources link. There is a newer list of features (Content Security Policy Level 2) that allows white listing inlined source scripts by SHA, but it is only supported in a very few browsers, so I recommend against it.
- Checkout this CSP header / string validator cspvalidator.org/
- Web frameworks like AngularJS require configuration to work with CSP ngCsp
- Vue.js framework also has a CSP-compatible build that does not use inline JS or styles. My personal website is using Vue.js with HTML fragments injected right into the DOM, CSP-compatible vue.js build allows to do this safely.
- What happens if the attacker replaces an external script?
- If an attacker can replace an external script served from your domain, or change a script served by a CDN then you have a big big problem. Your website is serving malware at this point and any client-side protection is useless. Secure your servers, download every external script via HTTPS.
- Can an attacker change the meta
Content-Security-Policytag, by injecting a new one for example?
- Yes, this is why you should include your own tag at the very top of the HEAD tag, or better send the same info using response headers.
- Can I verify that an external script is the one I receive?
- Use HTTPS and Subresource Integrity (SRI) where you specify SHA for the resource that you expect to receive. If CDN sends something different, the browser will refuse to run it.
- I just need an exception for a particular inline script, can I make an exception?
- There is the second level of content security specification (CSP2), which is not yet supported by most browsers. In CSP2 you can specify SHA for each allowed inline script. Other inline scripts will not execute be executed, since the SHAs will not be in the white list.
- Can you lock down allowed domains more precisely?
- Yes, instead of specifying just a domain like
<script src='https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js'></script>you can allow
https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/for example. Don't forget the trailing slash! You can even allow a specific file, like
srcipt-src: https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.jsto lock everything down.
- Yes, instead of specifying just a domain like