A website can have authentication, input validation, and secure cookies and still be vulnerable to cross-site scripting (XSS). One reason is that browsers may execute JavaScript that an attacker manages to inject into a page.
Consider an application that displays user comments. If the application handles untrusted input incorrectly, an attacker might inject a script that runs in another user's browser. Depending on the application and its security controls, this could expose sensitive information or perform actions as the affected user.
A Content Security Policy (CSP) helps reduce this risk by telling the browser which scripts and other resources it is allowed to load or execute. A nonce-based CSP takes this further by allowing specific inline scripts through a unique value generated for each HTML response.
This approach is useful for applications that need inline JavaScript but want to avoid allowing every inline script to run.
In this article, you will learn how CSP nonces work, how to configure them, how to use them in an ASP.NET Core application, and how to test the policy without breaking your website.
What Is a Content Security Policy?
Content Security Policy is a browser security mechanism that restricts the sources and types of content a page can load or execute.
A server normally sends CSP through the Content-Security-Policy HTTP response header. The browser reads the policy and applies its rules to supported resources.
For example:
Content-Security-Policy: default-src 'self'; script-src 'self'This policy uses the current origin as the default permitted source and allows scripts from that origin. It does not permit arbitrary inline scripts through the script-src directive.
That distinction matters because inline JavaScript can be a common route for XSS attacks.
Without a suitable policy, a page might contain:
<script>
alert("Inline script executed");
</script>A restrictive CSP blocks this inline script unless the policy explicitly permits it through a mechanism such as a nonce or hash, or through another applicable exception.
The exact behavior depends on the directives and browser support. CSP is a defense-in-depth control, not a replacement for safe output encoding, input handling, or secure application design.
What Is a CSP Nonce?
A nonce is a cryptographically random value generated by the server and included in both the CSP header and the HTML elements that are allowed to execute.
The term nonce means “number used once.” For CSP, the important requirements are that the value be unpredictable and newly generated for each HTML response.
Suppose the server generates this nonce:
r4nd0mExampleValueThe response header might contain:
Content-Security-Policy: script-src 'self' 'nonce-r4nd0mExampleValue'The HTML can then include an approved inline script:
<script nonce="r4nd0mExampleValue">
initializeApplication();
</script>The browser checks the script element against the policy. If the nonce matches the value in the policy, the script is permitted by that nonce source.
An inline script without the matching nonce is blocked by this policy unless another applicable rule allows it.
The example value above is illustrative only. A production nonce must be generated securely at runtime.
Why Nonces Are Better Than Allowing All Inline Scripts
A common way to make inline JavaScript work under CSP is to add 'unsafe-inline' to script-src.
For example:
Content-Security-Policy: script-src 'self' 'unsafe-inline'This weakens the protection offered by the script policy because it allows inline scripts without requiring each one to carry an approved nonce or hash. In some older browser implementations, the interaction between nonce sources and 'unsafe-inline' also differs from modern CSP behavior.
A nonce-based policy provides a more controlled option.
Approach | How it works | Security consideration |
|---|---|---|
| Permits inline scripts under the applicable policy | Broadly permits inline script execution |
Nonce | Permits scripts carrying a matching unpredictable value | Requires server-side generation and HTML integration |
Hash | Permits inline script content matching a configured cryptographic hash | Requires updating the policy when the script content changes |
External scripts only | Restricts scripts to approved sources | Requires careful source control and may not suit every application |
A nonce is not automatically safer in every implementation. If an attacker can inject arbitrary HTML and also obtain a valid nonce from the response, the protection may be undermined. The server must keep the nonce unpredictable and ensure that trusted templates, rather than untrusted user input, determine which scripts receive it.
How to Generate a Secure Nonce in ASP.NET Core
ASP.NET Core applications can generate a nonce using the cryptographic random number generator provided by .NET.
The following example creates a middleware component that generates a new nonce for each HTTP request and makes it available to downstream components.
using System.Security.Cryptography;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
var nonceBytes = RandomNumberGenerator.GetBytes(32);
var nonce = Convert.ToBase64String(nonceBytes);
context.Items["CspNonce"] = nonce;
await next();
});
app.Run();This example generates 32 random bytes and encodes them as Base64. It stores the result in HttpContext.Items, allowing downstream middleware or application code to access it during the same request.
The example does not yet send a CSP header. It only generates and stores the nonce.
In a real application, ensure that the nonce is generated before any component that needs it, and generate it for every HTML response that uses the nonce-based policy.
Avoid placing a fixed nonce in application configuration or source code. A fixed value would defeat the purpose of generating a fresh, unpredictable nonce.
Add the Nonce to the CSP Response Header
Once the nonce is available, the application can include it in the response policy.
For example, the middleware can be extended as follows:
using System.Security.Cryptography;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
var nonce = Convert.ToBase64String(
RandomNumberGenerator.GetBytes(32));
context.Items["CspNonce"] = nonce;
context.Response.OnStarting(() =>
{
context.Response.Headers["Content-Security-Policy"] =
$"default-src 'self'; " +
$"script-src 'self' 'nonce-{nonce}'; " +
$"object-src 'none'; " +
$"base-uri 'self'";
return Task.CompletedTask;
});
await next();
});
app.MapGet("/", () => Results.Content(
"<!doctype html><html><body><h1>Welcome</h1></body></html>",
"text/html"));
app.Run();This example demonstrates nonce generation and response-header construction. It does not include an inline script in the returned HTML, so there is no script element that uses the nonce yet.
The policy restricts scripts to the current origin and scripts carrying the generated nonce. It also blocks plugin-based objects and restricts the base URL used to resolve relative URLs.
The policy is intentionally small. A production website may need additional directives for stylesheets, images, fonts, API connections, frames, and other resources. Those directives should reflect the application's actual requirements rather than allowing every source by default.
Join the conversation! Your thoughts help the community grow.