Content-Security-Policy Deep Dive for NZ Sites
CSP deep dive for NZ sites: what unsafe-inline, unsafe-eval, nonces, hashes, wildcards and report-only mode do, with per-directive fixes for nginx and Apache.
Quick answer
Content-Security-Policy (CSP) tells the browser which sources of script, style, image, font, and connection are allowed on your site, and blocks everything else. The five building blocks worth understanding are default-src, script-src, style-src, frame-ancestors, and form-action — every other directive either falls back to default-src or fine-tunes what those five allow. The dangerous words to recognise are unsafe-inline, unsafe-eval, and * (wildcard); the modern replacements are nonces and hashes.
For the baseline header set and copy-paste fixes for the most common server stacks, see the Security Headers Explained guide. For a ten-second CSP check on your own site, run the Free Website Security Check.
How CSP actually works
Every time the browser loads a page, your server can send back a Content-Security-Policy header. The browser then enforces that policy for the entire page load: any script, style, image, font, frame, or network request that does not match an allowed source is blocked before it runs (1).
A policy is a list of directives. Each directive names a category of resource (script-src, img-src, font-src, connect-src, frame-src, …) and the sources it is allowed to come from. Sources can be:
'self'— your own origin (https://yourschool.nz)'none'— nothing is allowed'unsafe-inline'— any inline script/style is allowed (defeats most of what CSP is for)'unsafe-eval'— the browser may runeval(),new Function(),setTimeout(string, …)(1)https:/data:/blob:— any URL of that scheme (still wide)- An exact origin like
https://www.youtube.com— only this origin - A nonce like
'nonce-abc123'— only inline code that carries the matching nonce attribute - A hash like
'sha256-H8b8aH...='— only inline code whose contents hash exactly to that value
A request must match a directive that applies to its type; if no directive matches, the browser blocks it. This is why default-src matters — it is the fallback for every directive you do not set explicitly (2).
CSP does not fix vulnerabilities in your code. It limits what an attacker can do if something else (an XSS bug, a stale dependency, a vulnerable third-party widget) goes wrong. Think of it as a seatbelt — cheap, does not stop the crash, but changes the outcome.
The five directives that matter on a typical NZ school or small-business site
Most policy-writing guides list fifteen or twenty directives and leave you to pick. For a typical school or small-business site, only five matter; everything else either falls back to default-src or addresses an edge case you will probably never hit.
default-src — the catch-all
Content-Security-Policy: default-src 'self'
default-src is the fallback for every directive you do not set explicitly: if you do not set script-src, the browser uses default-src for scripts. Setting default-src 'self' means "by default, only allow things from my own domain". This is the right starting point for almost every site (2).
script-src — where CSP actually earns its keep
Content-Security-Policy: script-src 'self' https://www.googletagmanager.com
This is the directive that stops cross-site scripting. Anything in <script> tags, inline event handlers (onclick="…"), and JavaScript URLs (javascript:…) must come from one of the listed sources (1)(3).
The dangerous source here is 'unsafe-inline'. If your policy includes script-src 'unsafe-inline', the browser will execute any inline <script> tag regardless of what else your policy says. That is functionally the same as having no CSP at all for XSS purposes (3). If you see unsafe-inline in script-src, that part of the policy is doing nothing for security.
style-src — CSS and inline styles
Content-Security-Policy: style-src 'self' 'unsafe-inline'
style-src is more permissive in practice than script-src because a lot of legitimate tooling writes inline styles. unsafe-inline is generally accepted for style-src but should never appear in script-src (2).
frame-ancestors — the modern X-Frame-Options replacement
Content-Security-Policy: frame-ancestors 'none'
This directive replaces the older X-Frame-Options: DENY header. It stops your page being loaded inside an iframe on a different origin, which is the technique behind clickjacking attacks (4). 'none' is the strictest; 'self' allows your own pages to frame each other but no one else.
form-action — where forms can submit to
Content-Security-Policy: form-action 'self' https://forms.example.com
form-action controls where <form> elements are allowed to submit. This is the only directive that stops a cross-site request forgery (CSRF) attempt where the form lives on your page but submits to an attacker. Set it to your own origin plus any third-party form endpoints you actually use (1).
What unsafe-inline and unsafe-eval actually allow
These two source expressions are the most common reason a CSP fails to do its job.
unsafe-inline allows the browser to execute any inline <script> or <style>. Inline scripts are exactly the kind of payload a cross-site scripting (XSS) attack injects. Allowing them undoes most of CSP's value (3). The modern replacements are nonces and hashes (below).
unsafe-eval allows the browser to execute strings as code at runtime via eval(), new Function(), setTimeout("…", …), and similar. This matters because a lot of obfuscated or generated JavaScript uses these patterns. It is also sometimes required by older JavaScript bundlers and analytics libraries (1).
'unsafe-inline' 'unsafe-eval' to your CSP, treat it as a code-quality signal — that component is almost certainly using inline scripts or runtime code generation that you cannot easily audit.
Nonces — the modern replacement for unsafe-inline
A nonce is a random, single-request secret that the server generates for each page load and includes in both the CSP header and a matching attribute on every legitimate inline script. The browser only executes inline scripts that carry the matching nonce value (5).
A minimal nonce-based policy looks like this:
Content-Security-Policy: script-src 'self' 'nonce-RandomBase64Value=='
And every legitimate inline script on the page looks like this:
<script nonce="RandomBase64Value==">
// legitimate inline script
</script>
Two important properties:
- The nonce must be different every request. Reusing the same nonce allows an attacker who has injected one script to inject every subsequent script. Most server frameworks have a built-in helper for this — Express has
res.locals.cspNonce, Django has a CSP middleware, nginx can do it withsub_filterand a random value. - Nonces do not work with
<script>tags loaded from third-party CDNs unless those tags are rewritten. If you embed a third-party analytics snippet as<script nonce="…">, the third-party tag will work, but if the third party later adds their own inline script, it will be blocked.
Hashes — the same idea, but the secret is the script's contents
A hash is a SHA-256, SHA-512, or a SHA-256 of the script's exact text. You put the hash in the CSP, and the browser only executes inline scripts whose contents hash to that exact value (5).
Content-Security-Policy: script-src 'self' 'sha256-H8b8aH/3pQyN5w8t1u9oA2z5x0r3yL8qM9oA4z5x0r='
Hashes are useful when you cannot rotate a nonce per request — for example, in a fully static site or a CDN-cached HTML page. They are also useful as an audit tool: any inline script on the page either matches a hash in your policy or does not run, so you can list exactly which scripts your policy allows.
The downside is that any change to the inline script breaks the hash. Even a single space difference between what your source code says and what the browser sees will fail the check. For most teams, nonces are easier to operate.
Wildcards — what * actually allows
A wildcard source (*) matches every URL of the requested type. This is almost never what you want (2):
script-src *— any script from anywhere on the internet. Functionally no CSP for scripts.style-src *— any stylesheet. Same problem, lower stakes.frame-ancestors *— any site can frame yours. Defeats clickjacking protection.img-src *— any image source. Often acceptable, but be aware it allows tracking pixels.connect-src *— any fetch/XHR/WebSocket destination. Wide open.default-src *— wide open everywhere.
There are also subdomain wildcards like https://*.example.com and port wildcards like https://example.com:*. They are narrower than * but still broader than naming exact origins. Use specific origins whenever you can.

Per-directive copy-paste guidance
Below are the per-directive values that cover most NZ school and small-business sites. Build your policy up from default-src, then loosen the directives that need it.
The minimal starting policy
Content-Security-Policy: default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'
This policy allows nothing by default, locks <base> to your own origin, locks forms to your own origin, blocks framing, and blocks plugins. It will break most real sites, but it is the right starting point (1).
Adding scripts and styles
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
This is the recommended baseline for a school or small-business site that does not use a CDN, third-party analytics, or embedded videos. unsafe-inline only appears in style-src, where it is generally accepted because most legitimate CSS tooling relies on inline styles (2).
Adding third-party scripts (analytics, fonts, video)
For a site that loads Google Analytics, Google Fonts, and embeds YouTube:
Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://www.google-analytics.com; connect-src 'self' https://www.google-analytics.com; frame-src https://www.youtube.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
Each third party is named explicitly. Wildcards are not used.
Adding a nonce (for sites with inline scripts)
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-RandomBase64Value=='; style-src 'self' 'unsafe-inline'; …
The --style-src 'unsafe-inline' stays in place because CSS nonces are much harder to operate. The nonce in script-src must rotate per request — see the nonce section above for the operational details (5).
Report-only mode — how to roll out a CSP without breaking the site
Report-only mode tells the browser to send violation reports instead of blocking anything. It is the right way to roll out a new policy:
- Send the policy as
Content-Security-Policy-Report-Only(notContent-Security-Policy) — the browser will not block anything. - Add a
report-uriorreport-todirective pointing to a URL you control (a free hosted report collector, your own server, or a logging endpoint) (1). - Watch the reports for a week or two. You will see which resources the policy would have blocked.
- For each blocked resource, decide: do we need this resource (loosen the directive), or is it leftover from a plugin we no longer use (delete it)?
- Once the report volume is small and you understand every line, switch the header name from
-Report-Onlyto the enforcingContent-Security-Policy.
The Mozilla Observatory and the report-uri.io service are the most common free collectors. For NZ-hosted sites that want to keep data in the country, run the collector as a small endpoint on the same server or behind the same CDN.
Never leave report-only mode on forever. Report-only mode does not protect the site — it only logs what protection would have done. Once the violations are understood, switch to enforcement.nginx and Apache configuration
Below are the two most common server stacks for NZ schools and small businesses, with the CSP set as a header.
nginx
Add inside your server { … } block:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-RandomBase64=='; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
The always flag tells nginx to add the header to every response, including error responses. If you are rolling out report-only, change Content-Security-Policy to Content-Security-Policy-Report-Only and add a report-uri:
add_header Content-Security-Policy-Report-Only "default-src 'self'; …; report-uri https://reports.yourschool.nz/csp-violations" always;
Reload with nginx -s reload (or sudo systemctl reload nginx on systemd hosts).
Apache
Add inside your <VirtualHost>, or in an .htaccess in the document root:
<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-RandomBase64=='; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
</IfModule>
For report-only mode, use set Content-Security-Policy-Report-Only instead. If the header disappears on Apache, confirm mod_headers is loaded (apache2ctl -M | grep headers).
Common CSP mistakes to avoid
From running the free scanner across New Zealand school and small-business sites, the most frequent missteps are:
- Adding
'unsafe-inline'toscript-src"to make it work". Defeats the entire policy for XSS purposes. Use a nonce or hashes instead. - Adding
'unsafe-eval'because a vendor template asked for it. Most modern JavaScript does not need eval. Ask the vendor why before allowing it. - Wildcard
*inscript-srcorframe-ancestors. Functionally no CSP for that resource type. - Reporting endpoint set to a URL that returns 404. The browser will keep sending reports; nothing will happen to them. Confirm the endpoint returns 200.
- CSP set at the origin but stripped by a CDN. Cloudflare, Varnish, and most reverse proxies overwrite or drop headers set at the origin. Always verify through the CDN edge, not just from the origin server (see the Security Headers Explained guide for the broader header-stripping pattern).
- Report-only mode left on forever. See the callout above.
- Allowing
data:inscript-src. Inline data URIs can carry JavaScript. Only allowdata:inimg-srcif you actually use inline images.
Knowledge check
Test your understanding of Content-Security-Policy with these questions. Click or tap each card to flip it and see the answer.
default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'. A cross-site scripting bug is later discovered in a contact form plugin. What does the CSP actually stop here?tap to flipscript-src directive includes 'unsafe-inline', which allows the browser to execute any inline <script> tag regardless of source. The attacker's injected script is inline, so it runs. The rest of the policy still helps (it would still block a script loaded from a third-party domain, an inline event handler used as a script-injection vector in some browsers, and any other resources), but for the most common XSS pattern the CSP is doing nothing. Remove unsafe-inline from script-src and use a nonce or hashes instead (3).script-src 'self' 'nonce-RandomValue=='. The browser blocks every legitimate inline script on the page. What is the most likely cause?tap to flip<script> tag does not match the nonce in the policy. The two most common causes are: (1) the nonce is generated per request but the HTML is cached by a CDN or reverse proxy and served stale, so the HTML's nonce is from a previous request and does not match the response header; (2) the server template generates the nonce in the HTML but the CSP header is generated by a different component (nginx add_header, a middleware, a CDN) using a different random source. The two paths must agree on the same nonce value (5).script-src 'none') and check that the report for that fires. Only switch to enforcement once you have confirmed the reporting pipeline is alive and the volume of legitimate-resource violations is zero or accounted for (1).Sources and references
[1] Mozilla. (2025). MDN Web Docs — Content Security Policy (CSP). https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
[2] Google. (2025). Web Fundamentals — Content Security Policy. https://developers.google.com/web/fundamentals/security/csp
[3] OWASP Foundation. (2025). Cross-Site Scripting (XSS) Prevention Cheat Sheet. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
[4] OWASP Foundation. (2025). Clickjacking Defense Cheat Sheet. https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html
[5] Internet Engineering Task Force. (2025). CSP Level 3 — Nonces and hashes. https://www.w3.org/TR/CSP3/
[6] CERT NZ. (2025). Top 11 cyber security tips for your business. https://www.cert.govt.nz/business/guides/top-tips-for-cyber-security/
[7] Mozilla. (2025). Web security guidelines. https://infosec.mozilla.org/guidelines/web_security
What to do next
- Run the Free Website Security Check to see whether your site already sends a
Content-Security-Policyheader. The report shows the exact directive list the scanner received, so you can see whetherunsafe-inline,unsafe-eval, or wildcards are present. - For the broader set of recommended HTTP response headers (HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy) and the full copy-paste fixes for nginx, Apache, Express and Cloudflare, see the Security Headers Explained guide.
- For the secure-cookie side that CSP complements — HttpOnly, Secure, SameSite — see Secure Cookie Flags for NZ Sites.
- For the connection security (HTTPS) piece that everything else builds on, see HSTS Strict-Transport-Security Explained for NZ Sites.
- Content-Security-Policy Deep Dive for NZ Sites — what
unsafe-inline,unsafe-eval, nonces, hashes, wildcards and report-only mode actually do, with per-directive copy-paste fixes for nginx, Apache and Express. - Security Headers Explained for NZ Sites — the baseline header set: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, with copy-paste fixes for nginx, Apache, Express and Cloudflare.
- TLS Certificate Renewal Automation for NZ Sites — Certbot, Let's Encrypt, DNS-01 vs HTTP-01, and how to keep HTTPS certificates renewed automatically on NZ-hosted sites.
- Rate Limiting and Bot Protection for NZ Sites — why scan-probing differs from real attacks, with copy-paste nginx
limit_reqand fail2ban-style heuristics.