In this post, we're going to look at using Content Security Policy (CSP) as a defense-in-depth technique to block script injection attacks.
When building website that hosts user-generated content, such as:
Great to be here!
<script>window.location='https://example.com'</script>
It's necessary to encode user-generated content so that browsers don't mistake it for markup, and execute an untrusted script. This is easy to do for plain text, but what if a page needs to render user-generated HTML? Here's an example of HTML that contains inline Javascript, which browsers could execute:
<p>Great to <b>be</b> here!</p>
<img src="" onerror="alert(0)" />
<a href="javascript:alert(0)">Hi</a>
<script>window.location='https://example.com'</script>
This content must be sanitized before rendering. Libraries such as HTMLAgilityPack or DOMPurify provide a way to parse the HTML and strip out elements or attributes known to execute scripts.
Sanitization is important, but what if an attacker has discovered a way around the filter? This is where Content Security Policy comes in.
If the Content-Security-Policy
header is present when retrieving the page, and contains a script-src
definition, scripts will be blocked unless they match one of the sources specified in the policy. A policy might look something like:
script-src 'self'; object-src 'none'; base-uri 'none';
This policy disallows:
- External scripts not hosted on the same domain as the current page.
- Inline script elements, such as
<script>
- Evaluated Javascript, such as
<img src="" onerror="alert(0)" />
- Base elements, which could break scripts loaded from a relative path
- Object elements, which can host interactive content, such as Flash
Whitelisting inline scripts
Sometimes it is necessary to run inline scripts on your page. In these cases, the nonce
attribute on script
elements can be used to whitelist scripts that you control.
<script nonce="00deadbeef">doSomething()</script>
A matching nonce must be present in the CSP for the script to run. For compatibility with older browsers, unsafe-inline
allows scripts to run if the nonce
tag is unsupported.
script-src 'self' 'nonce-00deadbeef' 'unsafe-inline'; object-src 'none'; base-uri 'none';
It is critical that this nonce is derived from a cryptographic random number generator so that an attacker can't guess a future nonce. In .NET, RNGCryptoServiceProvider.GetBytes
can be used to fill a 16 byte array:
using (var random = new RNGCryptoServiceProvider())
{
byte[] nonce = new byte[16];
random.GetBytes(nonce);
return Convert.ToBase64String(nonce);
}
Whitelisting external scripts
strict-dynamic
can be used to allow scripts hosted on a third-party domain to be loaded by scripts that you control. However, at the time of writing, this isn't supported by all major browsers, so a host whitelist should be used as well as a fallback until it has broad support.
script-src 'self' 'nonce-00deadbeef' 'unsafe-inline' 'strict-dynamic' https://example.com; object-src 'none'; base-uri 'none';
When using strict-dynamic
, you will also need to add a nonce
to any external scripts that are referenced.
<script nonce="00deadbeef" src="https://example.com/analytics.js" />
Note:
Be careful what sources you whitelist. If any endpoints return JSONP and fail to sanitize the callback, it is possible to inject code. For example:
<script src="https://example.com/getuser?callback=window.location='http://google.com';test"></script>
Might return
window.location='http://google.com';test({})
in the JSONP response, which would cause arbitrary code to be executed!
There are other policies that you can define to strengthen your site's security, such as restricting where stylesheets are loaded from. This post only focuses on mitigating cross-site scripting attacks.
Further Reading
- CSP 3
- CSP Evaluator can be used to spot vulnerabilities in a policy.
- Introduction to CSP
- Mozilla Documentation
Thanks to Bradley Grainger and Kyle Sletten for reviewing this implementation.
Top comments (6)
Tremendous overview Dustin
Thanks Ben! I'm glad it was helpful :)
Agreed. It seems like browser add-ons might be a good fit for changing the page content or interacting with the page for most cases where you'd use a bookmarklet though :)
window.location='example.com'