Introduction
The Content Security Policy (CSP) header is generally supported in all modern browsers. It offers a way for website authors to restrict the resources that can be loaded by the page. This is a great tool to help secure your pages on the web, but applying CSP to an existing site can be tricky. Legitimate inline scripts are one pain point which will trigger CSP errors unless they are handled with care. In this blog post we look at the options we have at our disposal to handle inline scripts, and solve a particular problem with Safari.
Content Security Policy in brief
To make use of CSP as the author of a web page you will need to supply the Content-Security-Policy
HTTP
header in the response. This header has a value which is composed of one or more directives, where each directive is
separated by a semi-colon (;
).
Each directive specifies an allowlist of sources from which resources can be loaded in a particular context. For example,
the img-src
directive defines URLs from which you permit images to be loaded, and the
style-src
directive will ensure that stylesheets will only be loaded and executed if they come from one of
the allowlisted sources specified. In all cases, you can allow loading of resources from the same origin via the
'self'
source list value (quotes are significant here).
Let's look at a manufactured example (artificial line breaks are added for readability):
Content-Security-Policy: default-src 'self' cdn.example.com;
script-src 'self' https://apis.google.com;
style-src 'self' css.example.com;
connect-src 'self' https://api.example.com;
Here is a quick summary of what each of the directives included in this example is stipulating:
-
default-src 'self' cdn.example.com;
defines the default sources from which resources can be fetched. Most other directives will fall back to this allowlist if they aren't specified. In this case, by default, we allow resources to be loaded from the same origin that served the page, or fromcdn.example.com
. -
script-src 'self' https://apis.google.com;
defines which scripts the browser will permit. If the script comes from the same origin as served the page, or fromhttps://apis.google.com
, then the browser will load and execute the script. If your page attempts to load a script fromhttps://anything.else.com
, this will be blocked by the browser. -
style-src 'self' css.example.com;
: CSS stylesheets coming from the same origin or fromcss.example.com
will be loaded and applied. Otherwise the resource will be blocked. -
connect-src 'self' https://api.example.com;
limits the URLs which the browser will allow to be accessed via script interfaces which are capable of making HTTP requests, including mechanisms likefetch
,XMLHttpRequest
andWebSocket
. In this example, these interfaces will only be allowed to make requests to the same origin, or tohttps://api.example.com
.
The Problem: Inline scripts
Of the directives already mentioned we will focus on the script-src
directive.
Thus far everything is simple: we construct our CSP header with a list of domains from which our javascript can be loaded, javascript from other sources will neither be loaded nor executed. However, under such a policy, inline scripts will generate a CSP error. This is for good reason; if you know that your page should not have inline script tags, then there are a whole host of script-injection attacks that the browser can block on your behalf, it will simply refuse to execute any inline scripts on your page. We get an extra layer of security for no extra effort.
However, a world without inline scripts can be hard to imagine, especially if you are dealing with an existing project that is making heavy use of them. One particular pattern, which we have used multiple times in the past, is to use a simple inline script at the bottom of a page (or widget) to kick-off some JS initialization. Something like this:
<html lang="en">
<head>
<script src="/application.js"></script>
…
</head>
<body>
…
<script>
BlogApp.init();
</script>
</body>
</html>
However this pattern is not CSP-friendly as the <script>
tag at the bottom of the page will raise an
error in the browser as it violates your policy. So what are the options?
Nonces and hashes
Presuming we want to stick with the same pattern, and not have to rewrite our JS code, we have the following options:
We will rule out the first option (unsafe-inline
) because it will effectively negate much of the
benefit that the CSP is providing. By allowlisting all inline scripts we are potentially re-opening our
page to a bunch of injection attack vectors.
With that ruled out, lets look at nonces. A nonce is a one-off random number that is generated on your server, and associated with your response. When a request is received for a page the server needs to generate the random number using a cryptographically secure random number generator. It then attaches this nonce to your CSP header, like so:
Content-Security-Policy: default-src 'self' cdn.example.com;
script-src 'self' https://apis.google.com script-src 'nonce-blahblahblah'; …
In addition, the server should build the webpage and embellish any inline script tags with the nonce value, e.g.
<html lang="en">
<head>
<script src="/application.js"></script>
…
</head>
<body>
…
<script nonce="blahblahblah">
BlogApp.init();
</script>
</body>
</html>
With this arrangement the browser can ensure the inline script is only executed if it carries a nonce that matches the
value in the CSP header.
This is a neat solution to our problem. A single additional source is added to the CSP header and all legitimate in-line
scripts are allowlisted. Secure and convenient. It feels like a mirror of the CSRF token on form submissions; whereas the
CSRF token is verified on the server before action is taken, in this case the nonce is verified on the browser before action
is taken. In both cases the fly in the ointment is caching. A cached webpage can contain a CSRF token which is no longer
valid, likewise if the server caches portions of your rendered page, the nonce in the cached portion will become stale.
So how do we handle an inline script tag in a cached view (or fragment)? This is where we can use a hash
to allowlist our specific inline script.
Hash-based allowlisting really targets an individual script tag, you allowlist the script tag based on its specific content. To make use of this technique you will need to generate a base 64 encoded digest of the script tag you wish to allowlist, then you add this to your CSP header like so:
Content-Security-Policy: default-src 'self' cdn.example.com;
script-src 'self' https://apis.google.com script-src 'sha256-d9XnPH2UeLE4ZIxAW92XxljxF6iRBQ2hcRK747NtUU0=' …
In the example above I have used a SHA256 hash, but other algorithms are supported. By adding this hash value to your
CSP header you are telling the browser that should it encounter any script tag whose contents matches this digest, then
that script tag is to be trusted.
It can be useful to use the nonce- and hash-based systems simultaneously, with a single nonce-based taken covering most of your existing inline scripts, and separate hash-based digests covering a smaller number of specific script tags that are being inconvenienced by fragment caching. Why not use the hash technique for all of our inline scripts? Well the main drawbacks are:
- Each inline script requires a separate hash to be added to the CSP, which can make your CSP unwieldy
- Small changes to the inline script require that a new hash be generated, and your CSP updated
- How do you generate the hash?
Generating hash digest for a script
Working with Chrome, if we hit a case where a script has been blocked, the console error will conveniently tell us what hash should be added to allowlist the script:
I recently hit a problem where a particular inline script was only getting blocked on Safari. It was the typical case where the nonce token was being renedered invalid by caching, but for some reason the caching was only biting us on Safari:
Unlike with Chrome, the error reported on Safari, unfortunately, was not furnishing us with a digest for the script, so I needed to generate it.
Note: You will see other guidance (such as this previous reference) which will demonstrate a how to quickly generate a SHA from a string on the command line. But I found this approach to be problematic, because any whitespace is going to mess with your digest. If you are copy-and-pasting your code to the command line this operation becomes a little too flaky for me.
As an alternative I am suggesting you put those command-line tools away and just get your digest from the browser console. You pull the content directly from the script tag using DOM manipulation, and whitespace is taken care of for you. This is the snippet that I settled on for the task:
const scripts = document.getElementsByTagName("script"),
script_content = scripts[ scripts.length - 1 ].innerHTML,
enc = new TextEncoder(),
data = enc.encode(script_content);
crypto.subtle.digest('SHA-256', data).then(function(val){
const digest = ["sha256", _arrayBufferToBase64(val)].join('-');
console.log(`The digest for your script is: ${digest}`);
});
function _arrayBufferToBase64( buffer ) {
var binary = '';
var bytes = new Uint8Array( buffer );
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return window.btoa( binary );
}
Conveniently, the offending script tag in my case happened to be the last script tag on the page. However, you should
be able to take this code, alter it to extract the particular script tag you are interested in, copy it into your browser
console and it should echo out the hash for your script, which can then be added directly to your CSP header.
How does it work? The code above will identify the <script>
element and extract its contents to a
string, which we name script_content
. This string is then passed to a TextEncoder
instance. The encode
method on the
TextEncoder
will take
our character sequence as input and will generate a Uint8Array
containing a stream of UTF-8 bytes, as
this is the format we need to supply to the digest algorithm.
This byte stream is passed to the native
SubtleCrypto.digest()
method, this will generate a digest for our data. In the first argument we specify the algorithm we want to use (in this
case SHA-256
) and our byte stream is passed in the second argument. This digest function is
asynchronous, and will return an ArrayBuffer
containing the digest. So we register a callback that takes
the ArrayBuffer
returned by the algorithm and coverts it to a Base64 string, which we log to the console,
and which can be added directly to our CSP header.
The function _arrayBufferToBase64
has come courtesy of
this
answer on StackOverflow. I repeat it here verbatim. I constructs a Uint8Array
from the
ArrayBuffer
returned by the algorithm, and it iterates this array converting each byte to a corresponding
binary character. Finally the function invokes
window.btoa
to convert the binary string into a Base64 string.
This was a little more complicated than I had expected, and it certainly made me appreciate the convenience of having this value echoed out in Chrome's console error message. However, if you are ever stuck in a situation where you need to generate the SHA-256 digest for one of your inline scripts, this might just help you out in a pinch.
Summary
A well-written Content Security Policy can offer strong guarantees about the provenance of resources that are loaded on your site. However, certain existing coding patterns may not play well with your new bulletproof CSP. In this article we looked at how existing inline scripts could be supported within your CSP by using a nonce-based or hash-based approach. For the hash-based approach we saw how we could manually generate this hash from the console in the event that the browser isn't kind enough to spell it out for us.
Comments
There are no existing comments
Got your own view or feedback? Share it with us below …