Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP)
How to deploy a CSP based on script nonces or hashes as a defense-in-depth against cross-site scripting.
Why should you deploy a strict Content Security Policy (CSP)? #
Cross-site scripting (XSS)—the ability to inject malicious scripts into a web application—has been one of the biggest web security vulnerabilities for over a decade.
Content Security Policy (CSP) is an added layer of security that helps to mitigate XSS. Configuring a CSP involves adding the Content-Security-Policy HTTP header to a web page and setting values to control what resources the user agent is allowed to load for that page. This article explains how to use a CSP based on nonces or hashes to mitigate XSS instead of the commonly used host-allowlist-based CSPs which often leave the page exposed to XSS as they can be bypassed in most configurations.
A Content Security Policy based on nonces or hashes is often called a strict CSP. When an application uses a strict CSP, attackers who find HTML injection flaws will generally not be able to use them to force the browser to execute malicious scripts in the context of the vulnerable document. This is because strict CSP only permits hashed scripts or scripts with the correct nonce value generated on the server, so attackers cannot execute the script without knowing the correct nonce for a given response.
Why a strict CSP is recommended over allowlist CSPs #
If your site already has a CSP that looks like this: script-src www.googleapis.com
, it may not be effective against cross-site scripting! This type of CSP is called an allowlist CSP and it has a couple of downsides:
- It requires a lot of customization.
- It can be bypassed in most configurations.
This makes allowlist CSPs generally ineffective at preventing attackers from exploiting XSS. That's why it's recommended to use a strict CSP based on cryptographic nonces or hashes, which avoids the pitfalls outlined above.
Allowlist CSP
- Doesn't effectively protect your site. ❌
- Must be highly customized. 😓
Strict CSP
- Effectively protects your site. ✅
- Always has the same structure. 😌
What is a strict Content Security Policy? #
A strict Content Security Policy has the following structure and is enabled by setting one of the following HTTP response headers:
- Nonce-based strict CSP
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';

- Hash-based strict CSP
Content-Security-Policy:
script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
The following properties make a CSP like the one above "strict" and hence secure:
Uses nonces
'nonce-{RANDOM}'
or hashes'sha256-{HASHED_INLINE_SCRIPT}'
to indicate which<script>
tags are trusted by the site's developer and should be allowed to execute in the user's browser.Sets
'strict-dynamic'
to reduce the effort of deploying a nonce- or hash-based CSP by automatically allowing the execution of scripts that are created by an already trusted script. This also unblocks the use of most third party JavaScript libraries and widgets.Not based on URL allowlists and therefore doesn't suffer from common CSP bypasses.
Blocks untrusted inline scripts like inline event handlers or
javascript:
URIs.Restricts
object-src
to disable dangerous plugins such as Flash.Restricts
base-uri
to block the injection of<base>
tags. This prevents attackers from changing the locations of scripts loaded from relative URLs.
Adopting a strict CSP #
To adopt a strict CSP, you need to:
- Decide if your application should set a nonce- or hash-based CSP.
- Copy the CSP from the What is a strict Content Security Policy section and set it as a response header across your application.
- Refactor HTML templates and client-side code to remove patterns that are incompatible with CSP.
- Add fallbacks to support Safari and older browsers.
- Deploy your CSP.
You can use Lighthouse (v7.3.0 and above with flag --preset=experimental
) Best Practices audit throughout this process to check whether your site has a CSP, and whether it's strict enough to be effective against XSS.

Step 1: Decide if you need a nonce- or hash-based CSP #
There are two types of strict CSPs, nonce- and hash-based. Here's how they work:
- Nonce-based CSP: You generate a random number at runtime, include it in your CSP, and associate it with every script tag in your page. An attacker can't include and run a malicious script in your page, because they would need to guess the correct random number for that script. This only works if the number is not guessable and newly generated at runtime for every response.
- Hash-based CSP: The hash of every inline script tag is added to the CSP. Note that each script has a different hash. An attacker can't include and run a malicious script in your page, because the hash of that script would need to be present in your CSP.
Criteria for choosing a strict CSP approach:
Nonce-based CSP | For HTML pages rendered on the server where you can create a new random token (nonce) for every response. |
---|---|
Hash-based CSP | For HTML pages served statically or those that need to be cached. For example, single-page web applications built with frameworks such as Angular, React or others, that are statically served without server-side rendering. |
Step 2: Set a strict CSP and prepare your scripts #
When setting a CSP, you have a few options:
- Report-only mode (
Content-Security-Policy-Report-Only
) or enforcement mode (Content-Security-Policy
). In report-only, the CSP won't block resources yet—nothing will break—but you'll be able to see errors and receive reports for what would have been blocked. Locally, when you're in the process of setting a CSP, this doesn't really matter, because both modes will show you the errors in the browser console. If anything, enforcement mode will make it even easier for you to see blocked resources and tweak your CSP, since your page will look broken. Report-only mode becomes most useful later in the process (see Step 5). - Header or HTML
<meta>
tag. For local development, a<meta>
tag may be more convenient for tweaking your CSP and quickly seeing how it affects your site. However:- Later on, when deploying your CSP in production, it is recommended to set it as an HTTP header.
- If you want to set your CSP in report-only mode, you'll need to set it as a header—CSP meta tags don't support report-only mode.
Set the following A nonce is a random number used only once per page load. A nonce-based CSP can only mitigate XSS if the nonce value is not guessable by an attacker. A nonce for CSP needs to be: Here are some examples on how to add a CSP nonce in server-side frameworks: With a nonce-based CSP, every Blocked by CSP CSP will block these scripts, because they don't have Allowed by CSP CSP will allow the execution of these scripts if Option A: Nonce-based CSP
Content-Security-Policy
HTTP response header in your application:Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';Generate a nonce for CSP #
const app = express();
app.get('/', function(request, response) {
// Generate a new random nonce value for every response.
const nonce = crypto.randomBytes(16).toString("base64");
// Set the strict nonce-based CSP response header
const csp = `script-src 'nonce-${nonce}' 'strict-dynamic' https:; object-src 'none'; base-uri 'none';`;
response.set("Content-Security-Policy", csp);
// Every <script> tag in your application should set the `nonce` attribute to this value.
response.render(template, { nonce: nonce });
});
}Add a
nonce
attribute to <script>
elements #<script>
element must have a nonce
attribute which matches the random nonce value specified in the CSP header (all scripts can have the same nonce). The first step is to add these attributes to all scripts:<script src="/path/to/script.js"></script>
<script>foo()</script>nonce
attributes.<script nonce="${NONCE}" src="/path/to/script.js"></script>
<script nonce="${NONCE}">foo()</script>${NONCE}
is replaced with a value matching the nonce in the CSP response header. Note that some browsers will hide the nonce
attribute when inspecting the page source.
Set the following For several inline scripts, the syntax is as follows: All scripts that are externally sourced need to be loaded dynamically via an inline script, because CSP hashes are supported across browsers only for inline scripts (hashes for sourced scripts are not well-supported across browsers). Blocked by CSP CSP will block these scripts since only inline-scripts can be hashed. Allowed by CSP To allow execution of this script, the hash of the inline script must be calculated and added to the CSP response header, replacing the In the code snippet above, Option B: Hash-based CSP Response Header
Content-Security-Policy
HTTP response header in your application:Content-Security-Policy:
script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
object-src 'none';
base-uri 'none';'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'
.Load sourced scripts dynamically #
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script><script>
var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];
scripts.forEach(function(scriptUrl) {
var s = document.createElement('script');
s.src = scriptUrl;
s.async = false; // to preserve execution order
document.head.appendChild(s);
});
</script>{HASHED_INLINE_SCRIPT}
placeholder. To reduce the amount of hashes, you can optionally merge all inline scripts into a single script. To see this in action checkout the example and examine the code.Script loading considerations #
s.async = false
is added to ensure that foo executes before bar (even if bar loads first). In this snippet, s.async = false
does not block the parser while the scripts load; that's because the scripts are added dynamically. The parser will only stop as the scripts are being executed, just like it would behave for async
scripts. However, with this snippet, keep in mind:DOMContentLoaded
event before you append the scripts. If this causes a performance issue (because the scripts don't start downloading early enough), you can use preload tags earlier in the page.defer = true
won't do anything. If you need that behaviour, you'll have to manually run the script at the time you want to run it.
Step 3: Refactor HTML templates and client-side code to remove patterns incompatible with CSP #
Inline event handlers (such as onclick="…"
, onerror="…"
) and JavaScript URIs (<a href="javascript:…">
) can be used to run scripts. This means that an attacker who finds an XSS bug could inject this kind of HTML and execute malicious JavaScript. A nonce- or hash-based CSP disallows the use of such markup. If your site makes use of any of the patterns described above, you'll need to refactor them into safer alternatives.
If you enabled CSP in the previous step, you'll be able to see CSP violations in the console every time CSP blocks an incompatible pattern.

In most cases, the fix is straightforward:
To refactor inline event handlers, rewrite them to be added from a JavaScript block #
Blocked by CSP
<span onclick="doThings();">A thing.</span>
CSP will block inline event handlers.
Allowed by CSP
<span id="things">A thing.</span>
<script nonce="${nonce}">
document.getElementById('things')
.addEventListener('click', doThings);
</script>
CSP will allow event handlers that are registered via JavaScript.
For javascript:
URIs, you can use a similar pattern #
Blocked by CSP
<a href="javascript:linkClicked()">foo</a>
CSP will block javascript: URIs.
Allowed by CSP
<a id="foo">foo</a>
<script nonce="${nonce}">
document.getElementById('foo')
.addEventListener('click', linkClicked);
</script>
CSP will allow event handlers that are registered via JavaScript.
Use of eval()
in JavaScript #
If your application uses eval()
to convert JSON string serializations into JS objects, you should refactor such instances to JSON.parse()
, which is also faster.
If you cannot remove all uses of eval()
, you can still set a strict nonce-based CSP, but you will have to use the 'unsafe-eval'
CSP keyword which will make your policy slightly less secure.
You can find these and more examples of such refactoring in this strict CSP Codelab:
Step 4: Add fallbacks to support Safari and older browsers #
CSP is supported by all major browsers, but you'll need two fallbacks:
Using
'strict-dynamic'
requires addinghttps:
as a fallback for Safari, the only major browser without support for'strict-dynamic'
. By doing so:- All browsers that support
'strict-dynamic'
will ignore thehttps:
fallback, so this won't reduce the strength of the policy. - In Safari, externally sourced scripts will be allowed to load only if they come from an HTTPS origin. This is less secure than a strict CSP–it's a fallback–but would still prevent certain common XSS causes like injections of
javascript:
URIs because'unsafe-inline'
is not present or ignored in presence of a hash or nonce.
- All browsers that support
To ensure compatibility with very old browser versions (4+ years), you can add
'unsafe-inline'
as a fallback. All recent browsers will ignore'unsafe-inline'
if a CSP nonce or hash is present.
Content-Security-Policy:
script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
Step 5: Deploy your CSP #
After confirming that no legitimate scripts are being blocked by CSP in your local development environment, you can proceed with deploying your CSP to your (staging, then) production environment:
- (Optional) Deploy your CSP in report-only mode using the
Content-Security-Policy-Report-Only
header. Learn more about the Reporting API. Report-only mode is handy to test a potentially breaking change like a new CSP in production, before actually enforcing CSP restrictions. In report-only mode, your CSP does not affect the behavior of your application (nothing will actually break). But the browser will still generate console errors and violation reports when patterns incompatible with CSP are encountered (so you can see what would have broken for your end-users). - Once you're confident that your CSP won't induce breakage for your end-users, deploy your CSP using the
Content-Security-Policy
response header. Only once you've completed this step, will CSP begin to protect your application from XSS. Setting your CSP via a HTTP header server-side is more secure than setting it as a<meta>
tag; use a header if you can.
Limitations #
Generally speaking, a strict CSP provides a strong added layer of security that helps to mitigate XSS. In most cases, CSP reduces the attack surface significantly (dangerous patterns like javascript:
URIs are completely turned off). However, based on the type of CSP you're using (nonces, hashes, with or without 'strict-dynamic'
), there are cases where CSP doesn't protect:
- If you nonce a script, but there's an injection directly into the body or into the
src
parameter of that<script>
element. - If there are injections into the locations of dynamically created scripts (
document.createElement('script')
), including into any library functions which createscript
DOM nodes based on the value of their arguments. This includes some common APIs such as jQuery's.html()
, as well as.get()
and.post()
in jQuery < 3.0. - If there are template injections in old AngularJS applications. An attacker who can inject an AngularJS template can use it to execute arbitrary JavaScript.
- If the policy contains
'unsafe-eval'
, injections intoeval()
,setTimeout()
and a few other rarely used APIs.
Developers and security engineers should pay particular attention to such patterns during code reviews and security audits. You can find more details on the cases described above in this CSP presentation.
Further reading #
- CSP Is Dead, Long Live CSP! On the Insecurity of Whitelists and the Future of Content Security Policy
- CSP Evaluator
- LocoMoco Conference: Content Security Policy - A successful mess between hardening and mitigation
- Google I/O talk: Securing Web Apps with Modern Platform Features