IM.ContentSecurityPolicy · Umbraco 13 to 18

A strict CSP without manual nonces

Content Security Policy headers and a fresh nonce for every request, for ASP.NET Core and Umbraco. The package adds the nonce to your script, style and link tags.

Package
Installdotnet add package IM.ContentSecurityPolicy
Version2.0.4
Umbraco13 to 18
.NET8, 9, 10
What it does

Three jobs, on every request

A nonce per request

  • A random value from a cryptographically secure generator, available to your views and code.

A CSP header from your settings

  • Content-Security-Policy, or Report-Only while you test, built from options you control.

Nonces on your tags

  • Added to script, style and link tags by a tag helper, HTML helpers or view components.
Get started

Install and register

01

Add the package

One package for Umbraco 13 to 18 and plain ASP.NET Core.

dotnet add package IM.ContentSecurityPolicy

02

Register the middleware

In Program.cs. Nonce generation comes first, so the header can use the nonce.

builder.Services.AddNonceServices();

var app = builder.Build();

app.UseNonceGeneration();
app.UseContentSecurityPolicy();

03

Register the helpers once

In Views/_ViewImports.cshtml, so every view can add the nonce.

@addTagHelper *, IM.ContentSecurityPolicy
@using IM.ContentSecurityPolicy.Extensions

In your views

Four ways to add the nonce

01 Tag helper

Add asp-nonce="true" to script, style and link tags. The nonce of the current request is written for you.

<script src="~/js/site.js" asp-nonce="true"></script>

<style asp-nonce="true">
  .hero { color: #3544b1; }
</style>

<link rel="stylesheet" href="~/css/site.css" asp-nonce="true" />
02 HTML helpers

For tags you build in Razor. The helpers do not HTML-encode their input, so pass only trusted values.

@Html.ScriptWithNonce("~/js/site.js")
@Html.InlineScriptWithNonce("console.log('ready');")
@Html.StyleWithNonce("~/css/site.css")
@Html.InlineStyleWithNonce(".hero { color: #3544b1; }")

<script @Html.GetNonceAttribute()>
  window.dataLayer = window.dataLayer || [];
</script>
03 View components

NonceScript, NonceInlineScript, NonceStyle and NonceInlineStyle. Each needs its view, for example Views/Shared/Components/NonceScript/Default.cshtml.

@await Component.InvokeAsync("NonceScript", new { src = "~/js/site.js" })
04 Your own code

Inject INonceService. GetNonce() returns the nonce of the current request and HasNonce() tells you whether one exists.

public class ScriptsController(INonceService nonces) : Controller
{
    public IActionResult Index()
    {
        ViewData["Nonce"] = nonces.GetNonce();
        return View();
    }
}
The policy

Test first, then enforce

01

Report-Only while you test

The browser reports what the policy would block, and blocks nothing.

  • Set ReportOnly and a ReportUri
  • See which scripts and styles still lack a nonce
  • Visitors notice nothing

02

Enforce

Turn Report-Only off once the reports are clean.

  • The browser blocks anything without a valid nonce or allowed source
  • A fresh nonce on every request
  • Keep the ReportUri to catch regressions
Configuration

Set the sources

Options

Set the sources

{nonce} is replaced on every request.

builder.Services.AddNonceServices(options =>
{
    options.ScriptSrc = new[] { "'self'", "'nonce-{nonce}'", "https://cdn.example.com" };
    options.ReportOnly = true;
    options.ReportUri = "/csp-report";
});

Builder

Start from the strict preset

CspOptionsBuilder gives you a strict policy to extend.

var options = CspOptionsBuilder.CreateStrict()
    .WithScriptSrc("'self'", "'nonce-{nonce}'")
    .WithReportUri("/csp-report")
    .Build();
Compatibility

One package, Umbraco 13 to 18

Umbraco .NET Package version
13 LTS, 14 8 2.x
15, 16 9 2.x
17 LTS, 18 10 2.x
10 to 12 6, 7 1.0.2
In use

On our own site

01 · Response header

The policy ideamakers.pl sends

Scripts and styles run only from our own domain or with the nonce of the request. No 'unsafe-inline', no plugins, no framing by other sites.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-qTQx0elxVNpegG/7B+tAYA==' https://challenges.cloudflare.com;
  style-src 'self' 'nonce-qTQx0elxVNpegG/7B+tAYA==' 'sha256-Kh2l25Sy4DYc/g2Mm3KwI0W8tow97xzNP6zUE8/2UHE=';
  img-src 'self' data:;
  font-src 'self' data:;
  connect-src 'self';
  frame-src 'self' https://challenges.cloudflare.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'self'

02 · Two requests

A new nonce every time

The same page requested twice gets two different values, so a nonce copied from one response is useless in the next.

GET https://ideamakers.pl/en/contact/
  script-src 'self' 'nonce-74KxrM8xVoaTobkUhxHTZA==' ...

GET https://ideamakers.pl/en/contact/
  script-src 'self' 'nonce-qTQx0elxVNpegG/7B+tAYA==' ...

03 · Page source

The nonce on every script

Structured data, the consent state and inline scripts all carry the nonce of that response.

<script type="application/ld+json" nonce="qTQx0elxVNpegG/7B+tAYA==">
<script id="im-consent-state" type="application/json" nonce="qTQx0elxVNpegG/7B+tAYA==">
<script nonce="qTQx0elxVNpegG/7B+tAYA==">

Want a strict CSP on your Umbraco site?

We can set up the policy, move inline scripts to nonces and test it in Report-Only first.