When I first started implementing Content Security Policy (CSP) at work, I thought allowing JavaScript and CSS would be straightforward. I quickly discovered that it wasn't.
Questions started appearing one after another:
Why is my inline script blocked even though it's my own code?
Why doesn't adding a nonce to <link> work?
Why is my SHA-256 hash ignored?
What's the difference between the integrity attribute and a CSP hash?
When should I use a nonce? When should I use a hash?
Most articles explain these concepts separately. In this article, I'd like to share the practical lessons I learned while implementing CSP in a real ASP.NET Core application and explain when each mechanism should be used.
Why Do We Need Nonces and Hashes?
In the previous article, we saw that CSP blocks every script or stylesheet unless it has been explicitly authorized.
Consider the following page:
<script>
alert("Hello");
</script>
With this CSP:
Content-Security-Policy:
script-src 'self';
The browser blocks the script.
Why?
Because CSP cannot distinguish between:
From the browser's point of view, they're both simply inline code.
That's exactly why CSP introduced nonces and hashes: they allow you to explicitly tell the browser, "This piece of code is trusted."
Two Different Ways to Trust Inline Code
CSP offers two mechanisms.
The first is a nonce.
script-src 'self' 'nonce-AbCdEf123';
<script nonce="AbCdEf123">
console.log("Safe");
</script>
The browser compares the nonce in the CSP header with the nonce on the <script> element. If they match, the script executes.
The second mechanism is a hash.
script-src 'sha256-xxxxxx...';
Instead of comparing a random value, the browser computes the SHA-256 hash of the inline script itself.
If the computed hash matches the one declared in the CSP header, the script is allowed.
Nonce vs Hash
Although both authorize inline content, they solve different problems.
| Feature | Nonce | Hash |
|---|
| Generated | Every request | Once per content version |
| Changes | Every request | Whenever content changes |
| Best for | Dynamic pages | Static inline code |
A Common Misunderstanding
One of the first mistakes I made was assuming that adding a nonce everywhere would secure every resource.
For example:
<link
rel="stylesheet"
href="/css/site.css"
nonce="12345">
It looks reasonable.
Unfortunately, browsers completely ignore it.
The CSP specification only allows nonces on inline <script> and <style> blocks.
This means:
<style nonce="abc">
works.
But:
<link rel="stylesheet"
href="/site.css"
nonce="abc">
does absolutely nothing.
The stylesheet is authorized by its URL, not by a nonce.
What Actually Allows External Files?
For external resources, CSP simply checks whether the resource's URL matches one of the allowed sources.
style-src 'self';
allows
<link rel="stylesheet"
href="/css/site.css">
because the file comes from the same origin.
Similarly,
script-src 'self'
https://cdnjs.cloudflare.com;
allows
<script src="https://cdnjs.cloudflare.com/...">
No nonce is involved.
Don't Confuse CSP Hashes with SRI
This was probably the most confusing part during implementation.
Both use SHA-256.
Both look almost identical.
Yet they solve completely different problems.
Suppose you load Bootstrap from a CDN.
<scriptsrc="https://cdn..."integrity="sha256-xxxxx"></script>
This is Subresource Integrity (SRI).
Its purpose is to verify that the downloaded file hasn't been modified by the CDN or by an attacker.
The browser downloads the file, computes its SHA-256 hash, compares it with the integrity attribute, and rejects the resource if they don't match.
CSP hashes are different.
They authorize inline code.
script-src 'sha256-xxxxx';
Here, the browser hashes the content inside the <script> block and compares it with the hash declared in the CSP header.
Although both mechanisms use SHA-256, they protect different things.
| Mechanism | Protects |
|---|
| CSP Hash | Inline <script> / <style> |
SRI (integrity) | External downloaded resources |
One does not replace the other.
Why We Chose Nonces
During my work on an ASP.NET Core application, several HTML pages were modified dynamically before being sent to the browser.
For example, values such as OAuth client IDs were injected into inline JavaScript.
A simplified example looks like this:
<script>ui.initOAuth({
clientId: "tenant-123"
});
</script>
The content depends on the current request.
That means the script changes.
Since the content changes, its SHA-256 hash changes as well.
Maintaining CSP hashes would have been impractical because a new hash would be required for every variation.
Instead, generating a new nonce for each request solved the problem automatically.
When Hashes Become Painful
Imagine you authorize this inline script with a CSP hash.
<script>console.log("Version 1");
</script>
Later you simply change it to:
<script>console.log("Version 2");
</script>
Nothing else changed.
Yet the browser blocks it.
Why?
Because changing even a single character produces a completely different SHA-256 hash.
Someone must recompute the hash and update the CSP header.
This quickly becomes difficult to maintain in large applications.
What About unsafe-inline?
Sometimes developers solve CSP errors by doing this:
script-src 'self' 'unsafe-inline';
It fixes the issue immediately.
It also defeats one of CSP's biggest security benefits.
unsafe-inline tells the browser:
"Execute every inline script you encounter."
That includes attacker-injected JavaScript.
If you are already using nonces or hashes, adding unsafe-inline is unnecessary and weakens your policy.
Modern browsers will also ignore it when strict-dynamic is used.
A Special Case: style="" Attributes
Nonces work for:
<style nonce="...">
They do not work for:
<div style="color:red">
Inline style attributes cannot receive a nonce.
If you absolutely need to allow them, CSP requires using hashes together with the unsafe-hashes keyword.
Which One Should You Use?
As a rule of thumb:
| Scenario | Recommendation |
|---|
| Static inline script | Hash or Nonce |
| Dynamic inline script | Nonce |
Inline <style> block | Nonce |
| External JavaScript | 'self' or allowed origin (optionally SRI) |
| External CSS | 'self' or allowed origin (optionally SRI) |
Inline style="" attribute | Hash + unsafe-hashes (only if unavoidable) |
In most modern ASP.NET Core applications, nonces are the preferred solution. They require less maintenance, work naturally with dynamically generated pages, and scale well as the application evolves.