What XSS Is and Why CSP Matters
Cross-site scripting (XSS) is an attack where malicious JavaScript is injected into a trusted web page. The injected script runs in the victim's browser with the same privileges as the legitimate page — reading cookies, making API requests, exfiltrating data, redirecting users.
Traditional defences — sanitising user input, encoding output — are necessary but not sufficient. A single unescaped value, a third-party library with a vulnerability, a misconfigured template can all create an XSS opening.
A Content Security Policy is a second line of defence at the browser level. Even if a script is injected, CSP tells the browser which scripts are allowed to execute. Injected scripts that don't come from approved sources simply don't run.
How CSP Works
CSP is delivered as an HTTP response header (or a <meta> tag, though the header is preferred):
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com
The browser reads the policy, and for every resource the page tries to load — scripts, stylesheets, images, fonts, API requests — checks whether the source is permitted. Violations are blocked and optionally reported.
Core Directives
**default-src** — the fallback for directives not explicitly specified. A reasonable starting point: default-src 'self' — all resources must come from the same origin.
'self'— only scripts from the same origin'none'— no scripts at allhttps://cdn.example.com— a specific domain'nonce-{random}'— scripts with a matching nonce attribute
**style-src** — controls CSS sources. Inline styles are blocked by default.
**img-src** — controls image sources. img-src 'self' data: allows images from the same origin and data URIs.
**connect-src** — controls which URLs can be contacted via fetch, XMLHttpRequest, WebSocket.
**frame-src** — controls which origins can be embedded in frames. frame-src 'none' prevents your page from being framed (reduces clickjacking risk).
**object-src** — controls plugins (<object>, <embed>). Always set to 'none' unless you have a specific reason.
**base-uri** — restricts the <base> element. Set to 'none' or 'self' to prevent base tag injection attacks.
The Nonce Pattern (Correct Way to Allow Inline Scripts)
Inline scripts (<script>alert('hello')</script>) are blocked by a strict CSP. The correct way to allow specific inline scripts is a nonce: a random value generated per request, included in the CSP header and in the script tag's nonce attribute.
Server generates: const nonce = crypto.randomBytes(16).toString('base64')
Header: script-src 'nonce-abc123def456'
HTML: <script nonce="abc123def456">...</script>
An attacker injecting a script tag cannot know the nonce (it changes every request), so the injected script is blocked even though inline scripts with the correct nonce are allowed.
**Never use 'unsafe-inline'** — this allows all inline scripts and negates most of CSP's XSS protection. It's often added to "fix" a blocked script but defeats the purpose.
`'unsafe-eval'` and eval()
eval() and related functions (Function(), setTimeout with a string argument) execute arbitrary JavaScript from strings. CSP blocks these by default. Some libraries (notably Google Tag Manager and older Angular versions) require 'unsafe-eval', which is a significant weakening of the policy.
If your CSP requires 'unsafe-eval', consider whether that library is necessary or whether an alternative approach exists.
Report-Only Mode for Safe Deployment
Deploy CSP with a report-only header first:
Content-Security-Policy-Report-Only: [policy]; report-uri https://your-reporting-endpoint.com/
This logs all violations without blocking anything, letting you discover what your policy would break before enforcing it. Once violations are resolved, switch to the enforcing header.
A Practical Starter Policy
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{per-request-nonce}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none'
style-src 'unsafe-inline' is a pragmatic concession — blocking inline styles breaks many UI frameworks. frame-ancestors 'none' is a clickjacking defence equivalent to the older X-Frame-Options header.
NoxaKit's Content Security Policy Builder generates a policy from your configuration. The HTTP Security Headers Checker verifies which security headers your site is currently serving.