Content-Security-Policy example for a SaaS, with a safe rollout
A nonce-based starter Content-Security-Policy for a SaaS, the report-only rollout that shows every violation first, and the Next.js proxy setup.
LLaunchScaler·Published ·8 min read
A good starter Content-Security-Policy for a SaaS allows scripts only by a per-request nonce, blocks plugins and framing, and names every other source your app loads: default-src 'self'; script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'. Ship it first as Content-Security-Policy-Report-Only, so the browser reports every violation without blocking anything, then switch to the enforcing header once the reports are clean.
That order matters more than the exact policy. An enforced CSP that is wrong breaks sign-up, checkout or analytics the moment it deploys; a report-only one tells you what would have broken.
What does a starter Content-Security-Policy look like?
Start from the strict, nonce-based policy web.dev recommends and add the non-script sources your pages load. The policy below is a report-only starting point for a typical SaaS app: your own origin, inline styles, images from anywhere over HTTPS, and API calls to your own backend.
The header is one line in practice; it is split here for reading. What each directive does:
Directive
What it controls
Why this value
default-src 'self'
Questions, answered
What people ask about this
01
What is a good Content-Security-Policy example?
A strong starting point is script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'self'; frame-ancestors 'none', plus default-src 'self' and the image, style and connect sources your app uses. Generate a new nonce for every response.
Only scripts carrying this response's nonce, plus scripts they load
https: 'unsafe-inline' in script-src
Fallback for old browsers
Browsers that support 'strict-dynamic' ignore both, per web.dev
style-src 'self' 'unsafe-inline'
Stylesheets and inline styles
Most UI libraries inject inline styles; tighten later
img-src 'self' data: https:
Images
Avatars and user content often come from other hosts
connect-src 'self'
fetch, XHR, WebSocket targets
Add your API, analytics and error-tracking origins
object-src 'none'
Plugins (<object>, <embed>)
Nothing modern needs them
base-uri 'self'
The <base> tag
Stops an injected <base> rewriting your relative URLs
form-action 'self'
Where forms may submit
Stops injected forms posting elsewhere
frame-ancestors 'none'
Who may frame your pages
Clickjacking protection; replaces X-Frame-Options
upgrade-insecure-requests
http:// subresources
Browser upgrades them to https://
report-uri and report-to
Where violation reports go
Both, so older and newer browsers report
How does Mozilla Observatory score a Content-Security-Policy?
The Observatory takes 25 points for no CSP, 20 for 'unsafe-inline' or data: in script-src, and 10 for 'unsafe-eval', from a starting score of 100. A policy with none of those earns 5 bonus points, and one built on default-src 'none' with a locked-down form-action earns 10. A report-only header on its own scores the same as no CSP.
What the Observatory finds
Modifier
No CSP header
-25
Only Content-Security-Policy-Report-Only
-25
Header cannot be parsed
-25
'unsafe-inline' or data: in script-src, or https: in script-src or object-src
-20
An http: source for scripts or other active content on an HTTPS site
-20
'unsafe-eval'
-10
An http: source for images or media only
-10
Unsafe sources in style-src only
0
No unsafe sources
+5
default-src 'none', no unsafe sources, form-action set to 'none' or 'self'
+10
Two details from the Observatory's CSP analyzer explain why the starter policy above is not penalised for its fallbacks. It drops 'unsafe-inline' from script-src when a nonce or hash is present, and when 'strict-dynamic' sits beside a nonce it also drops https: and 'self' before scoring. Without the nonce, those same tokens cost you 20 points.
How do you roll out a CSP without breaking the site?
Deploy the policy as report-only, collect violation reports from real traffic, fix or allow each source they name, and only then send the enforcing header. MDN describes the report-only header as the way "to test or repair violations before a specific Content-Security-Policy is applied and enforced."
List what your pages load. Open the Network panel on your main page types (marketing page, sign-up, the app, checkout) and note every script, style, font, image, frame and API origin.
Add a report endpoint. Define it with Reporting-Endpoints: csp-endpoint="https://yourapp.com/api/csp-report", then reference it with report-to csp-endpoint. Keep report-uri beside it: MDN marks report-uri as deprecated but advises sending both until report-to is broadly supported, and browsers that support report-to ignore report-uri.
Accept both report formats at that endpoint. report-uri posts Content-Type: application/csp-report with a csp-report object (blocked-uri, effective-directive, document-uri); report-to posts application/reports+json with type: "csp-violation" and a body (blockedURL, effectiveDirective, documentURL, disposition). Return 204 and log them.
Ship the header as Content-Security-Policy-Report-Only in production. Nothing is blocked, so users see no change.
Group reports by effectiveDirective and blocked URL. For each, either add the origin to the matching directive, or remove the code that loads it.
When the only reports left are noise (browser extensions inject scripts and generate reports you cannot fix), switch the header name to Content-Security-Policy and keep the report endpoints.
Treat reports as untrusted input. MDN warns that violation reports "should be considered attacker-controlled data," so sanitize them before storing or showing them in a dashboard.
How do nonces and 'strict-dynamic' work?
A nonce is a random value you generate for each response and put in two places: the CSP header ('nonce-abc123') and a nonce="abc123" attribute on every <script> you trust. An injected script cannot know the value, so it will not run. 'strict-dynamic' then lets those trusted scripts load further scripts without listing every CDN.
web.dev's requirements for the nonce:
A cryptographically strong random value, ideally 128 bits or more.
Newly generated for every response, never reused or cached.
Base64 encoded.
The code that breaks under a nonce policy is predictable: inline event handlers such as onclick="..." in your HTML, and javascript: URLs. web.dev lists both as patterns to refactor into addEventListener calls before you enforce. A hash-based policy ('sha256-...') is the alternative for fully static pages that cannot generate a nonce per request.
How do you add a nonce-based CSP in Next.js?
Generate the nonce in proxy.ts (the file was called middleware.ts before Next.js 16), put it in the CSP header on both the request and the response, and Next.js attaches it to its own framework scripts, page bundles and inline scripts automatically. The trade-off is that every page using the nonce must render dynamically.
In development, add 'unsafe-eval' to script-src, because React uses eval for debugging information. Production does not need it.
Use a matcher to skip _next/static, _next/image, favicon.ico and prefetch requests, which do not need the header.
Read the nonce in a Server Component with (await headers()).get('x-nonce') and pass it to any <Script nonce={nonce}> you load, such as a tag manager.
Pages must render dynamically (call await connection() if one would otherwise be static). Static optimisation and ISR stop working for those pages, CDN caching needs extra setup, and Partial Prerendering is incompatible with a nonce-based CSP.
For the report-only phase, send the response header as Content-Security-Policy-Report-Only instead. The nonce keeps working, because Next.js's renderer reads it from either request header: it checks content-security-policy, then content-security-policy-report-only.
If the dynamic-rendering cost is too high for your marketing pages, Next.js has an experimental alternative for the App Router: experimental.sri adds integrity hashes to your scripts at build time, so a CSP without nonces can still drop 'unsafe-inline' while pages stay static.
Why use frame-ancestors instead of X-Frame-Options?
frame-ancestors is the CSP directive that controls who may embed your page, and it accepts a list of origins, which X-Frame-Options cannot. Set frame-ancestors 'none' unless someone must frame you, and keep X-Frame-Options: DENY beside it for older browsers.
MDN notes three rules that trip people up. frame-ancestors is not supported in a <meta> CSP, so it only works as a response header. It does not fall back to default-src, so leaving it out allows framing by anyone. And with nested frames, every ancestor must match the list. The security headers checklist has the rest of the headers to send beside the CSP.
What are the common CSP violations and their fixes?
Most violations in the first reports come from a handful of sources, and each maps to one directive. Fix the code where you can, and add an origin to a directive only when you trust it and need it.
Report says
Usual cause
Fix
script-src-elem blocked, blockedURL: inline
An inline <script> without the nonce
Add the nonce, or move the code to a file
script-src-attr blocked
An onclick= or other inline handler
Replace with addEventListener
connect-src blocked
Analytics, error tracking or an API on another origin
Add that exact origin to connect-src
img-src blocked, http:// URL
An old image URL on an HTTPS page
Change it to https://; see the mixed content guide
frame-src blocked
A payment, video or scheduling embed
Add the provider's origin to frame-src
style-src-elem blocked
A CSS-in-JS library injecting <style>
Pass it the nonce, or keep 'unsafe-inline' in style-src for now
font-src blocked
A web font from a font CDN
Add the font host to font-src, or self-host
upgrade-insecure-requests quietly fixes many http:// image and media URLs, but it is a stopgap; the mixed content guide shows how to find and fix the URLs themselves. AI-built apps tend to ship with no CSP at all, which is one of the gaps in the vibe-coded app security checklist.
Get the exact CSP findings for your site
LaunchScaler's free scan already reads your CSP: whether one is sent at all, whether script-src allows 'unsafe-inline' or 'unsafe-eval', and whether frame-ancestors or X-Frame-Options protects against framing. It runs with no account as part of 156 checks across 6 of its 7 categories. The full audit, $19 once for the domain, opens every one of those checks to its evidence (the header value as your server sent it) and the exact fix, and it adds 40 more checks. Run the free scan first, then open the full audit from your report.
It is a header with the same syntax as Content-Security-Policy that reports violations without blocking anything. Ship it first, read the reports, fix or allow each source, then switch to the enforcing header.
03
Is 'unsafe-inline' in a CSP bad?
In script-src, yes: it lets any injected inline script run, and Mozilla's Observatory deducts 20 points for it. Use nonces or hashes instead. In style-src it is a smaller risk, and the Observatory does not penalise it there.
04
Can I set a CSP in a meta tag?
Partly. A meta http-equiv CSP works for most directives, but frame-ancestors and report-uri are not supported in a meta element, and Content-Security-Policy-Report-Only cannot be set that way at all. Use a response header.
05
Does a report-only CSP count in security scanners?
Not as protection. Mozilla's Observatory scores a site with only Content-Security-Policy-Report-Only the same as a site with no CSP, a 25-point penalty, because nothing is enforced.
The assessment fails when LCP, INP or CLS misses Good at the 75th percentile of 28 days of real Chrome data. How to read it and which metric to fix first.