Content Security Policy: prevent script injection without breaking your site
Configure CSP resource rules, nonces, hashes, strict-dynamic, and violation reporting, then roll out an enforced policy with representative browser testing.
Content Security Policy (CSP) lets a site tell the browser which resources it may load and which scripts it may execute. A well-designed policy can block injected JavaScript, restrict connections, and limit where a page can be embedded. Deliver it through an HTTP response header and test it against the actual application before enforcing it.
CSP does not automatically deny everything you omit. A directive restricts its own resource type, and default-src supplies a fallback for many fetch directives. Other controls, including base-uri, form-action, and frame-ancestors, must be declared separately.
How CSP reduces XSS exposure
Cross-site scripting (XSS) lets an attacker run JavaScript with a site's privileges. That code may alter the page, make authenticated requests, or read cookies that are accessible to JavaScript. An appropriate script-src policy can stop a script injected from an unapproved origin from loading, even if the application mistakenly includes its tag in the HTML.
That protection has limits. An allowed script source can be compromised, and trusted scripts can contain exploitable behavior. CSP belongs alongside context-sensitive output encoding and HTML sanitization, not in place of them.
CSP Level 1 established the basic resource restrictions. Level 2 added nonce and hash sources; Level 3 extends the model with features such as strict-dynamic. Support varies by feature and browser, particularly for reporting. Test the browsers your application supports. The W3C CSP specification defines the policy model.
Read a policy
Content-Security-Policy: default-src 'self'; img-src *; script-src 'self' https://trusted.cdn.example;
This illustrative policy allows scripts from the document's own origin and the named HTTPS CDN. Its image rule is broad: * allows network sources under the relevant matching rules, but does not simply enable every scheme, such as data:. Other fetch types fall back to default-src 'self' where the specification defines that fallback.
Source expressions include 'self' for the current origin, 'none' to allow no source, exact origins, wildcard subdomains, and schemes such as https:. https://*.example.com covers subdomains; it does not also mean https://example.com. Broad source lists increase what the browser will trust.
| Header | Effect |
|---|---|
Content-Security-Policy |
Enforces the declared policy and can report violations. |
Content-Security-Policy-Report-Only |
Reports violations of that policy without blocking them. A separate enforced policy still applies. |
Reporting requires a configured destination, and delivery is best effort. Sending multiple enforced policies does not combine them into a larger allowlist: a resource must satisfy every applicable policy.
Start with explicit resource rules
default-src 'self' is a useful starting point, but it is neither a complete policy nor the strictest possible one. It also prevents ordinary inline scripts and styles through the applicable fallback unless another directive permits them.
Here is an Express middleware example for a page that uses a particular analytics script and Google Fonts. Install Express with npm install express and save this as server.cjs:
const express = require('express');
const app = express();
const policy = [
"default-src 'self'",
"script-src 'self' https://analytics.example.com",
"style-src 'self' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
].join('; ');
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', policy);
next();
});
app.get('/', (req, res) => res.send('<h1>CSP example</h1>'));
app.listen(3000, '127.0.0.1');
The array is for readability; the response sends a single header value. Replace the illustrative analytics origin with a reviewed dependency, or remove it. Allowing Google Fonts' stylesheet origin does not automatically allow its font files: those need the separate font-src rule shown here. The image rule deliberately allows any HTTPS image source; narrow it if the application does not need that scope.
Common directives
| Directive | What it controls | Example |
|---|---|---|
default-src |
Fallback for fetch directives that define one | 'self' |
script-src |
Script loading and execution | Nonces or hashes for trusted scripts; carefully selected sources where needed |
style-src |
Stylesheets and inline styles | 'self' and the required stylesheet origin |
img-src |
Images | 'self' data: https: when that scope is intended |
connect-src |
Connections such as fetch, XHR, and WebSocket | Explicit API origins and any required wss:// origin |
font-src |
Fonts | 'self' and the font file origin |
object-src |
Embedded content loaded through elements such as object and embed |
'none' |
frame-ancestors |
Which ancestors may embed the page | 'none', or explicitly approved origins |
base-uri |
URLs allowed in the document's base element |
'self' or 'none' |
form-action |
Form submission destinations | 'self' |
frame-ancestors needs a response header; it is not supported in a CSP meta element. It also does not inherit default-src. See the directive definitions.
Allow intended inline scripts
When the effective script policy restricts inline execution, adding 'unsafe-inline' broadly weakens that protection. Prefer a nonce for dynamically rendered pages or a hash for script content that stays constant. These are different tools, rather than successive levels on a universal security scale.
Generate a fresh nonce for every response
A nonce must be unpredictable and generated by a cryptographically secure random source. The same value belongs in that response's policy and its trusted script elements. This is an alternative complete Express example:
const express = require('express');
const { randomBytes } = require('node:crypto');
const app = express();
app.get('/', (req, res) => {
const nonce = randomBytes(16).toString('base64');
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
`script-src 'nonce-${nonce}'`,
"object-src 'none'",
"base-uri 'none'",
"frame-ancestors 'none'",
"form-action 'self'",
].join('; '));
res.send(`<!doctype html><title>Nonce example</title>
<script nonce="${nonce}">document.title = 'Trusted script ran';</script>`);
});
app.listen(3000, '127.0.0.1');
Only server-authored script elements should receive the nonce. Do not scan arbitrary HTML and add it to every script: that could authorize injected content. Ensure caches do not reuse a nonce across separately generated responses or serve an HTML/header pair with different values. The nonce requirements explain the unpredictability requirement.
Hash static script content
A hash authorizes exact script bytes. For example, compute the SHA-256 source expression for console.log('ready');:
node -e "process.stdout.write('sha256-' + require('node:crypto').createHash('sha256').update(\"console.log('ready');\").digest('base64') + '\n')"
Put the resulting expression inside single quotes in script-src. The corresponding inline element is:
<script>console.log('ready');</script>
Whitespace inside the element changes the hash. Hash the script contents without the opening and closing tags. Updating that script requires updating its policy hash too. This example covers inline scripts; external script hashes have additional integrity requirements.
Use strict-dynamic for trusted script loaders
Adding 'strict-dynamic' to a nonce- or hash-based script-src policy lets trusted scripts load further non-parser-inserted scripts. It is useful for a loader that creates script elements dynamically. It does not mean every script in the document becomes trusted.
In supporting browsers, host sources and expressions such as 'self' no longer govern those script loads when strict-dynamic applies. Review how the trusted loader chooses URLs: accepting an attacker-controlled script URL can defeat the intended boundary. See strict-dynamic behavior.
Upgrade insecure resource URLs carefully
upgrade-insecure-requests tells the browser to upgrade applicable HTTP URLs to HTTPS. The target must actually support HTTPS; the browser does not fall back to HTTP when the upgraded request fails. This directive does not provision TLS or replace HSTS. Check upgraded dependencies before enabling it.
Collect violation reports
Use the current Reporting API endpoint header together with report-to. A legacy report-uri destination can provide compatibility for browsers that do not support the newer path:
Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; object-src 'none'; base-uri 'self'; report-to csp; report-uri /csp-violations;
These endpoint addresses are illustrative: deploy a receiver at your own destinations. The current header is Reporting-Endpoints; the older Report-To JSON header is not the same mechanism. Browsers that support report-to use it instead of report-uri. See the Reporting API and CSP reporting directives.
Reports can identify the page, blocked resource, effective directive, and policy disposition. They are useful evidence of a broken dependency or suspicious injection, but a report spike is not proof of an attack. Browser extensions and ordinary releases can also cause violations, and not every violation will reach the receiver.
Treat report bodies as untrusted input. Limit request size and rate, validate the expected report formats, and strip sensitive URL query values before storage. The legacy CSP report envelope differs from Reporting API batches, so a receiver supporting both must handle both formats.
Roll out the policy in stages
- Deploy the intended candidate policy in report-only mode. An overly permissive policy tells you little about what a stricter one would break.
- Exercise representative routes, authenticated workflows, devices, browsers, and third-party integrations. Investigate reports before adding an origin.
- Enforce the tested policy gradually and watch application errors alongside violations. A quiet week alone does not prove complete coverage.
- Continue monitoring after releases and dependency changes. Keep a tested rollback path if a policy disrupts an essential workflow.
For single-page applications, include the required API origins in connect-src and explicitly configure WebSocket origins where needed. A third-party widget may require several resource types, such as scripts, frames, styles, and connections. Review that footprint instead of assuming one script allowlist entry covers it.
If the report receiver writes normalized JSON records to a file, Guance's log collector can collect them. Group new violations by release and directive, retain the sanitized page/resource context, and verify sample records before building alerts. Report receipt and collection add their own delivery delays.
Common questions
What if enforcing CSP breaks part of the site?
Inspect the blocked resource and effective directive, then reproduce the affected workflow. Fix the dependency or make a narrowly scoped policy change. If necessary, roll back enforcement while investigating. Breakage can occur after a previously successful report-only trial because code or coverage changed.
Does CSP replace escaping or sanitization?
No. Continue context-appropriate output encoding and sanitization of allowed HTML. HttpOnly cookies reduce JavaScript access to cookies, but do not prevent injected scripts from making authenticated requests.
Should I use report-uri or report-to?
Use report-to with Reporting-Endpoints for the current mechanism, and consider report-uri as a deprecated compatibility fallback. Validate behavior in supported browsers and treat reports as best-effort telemetry.
The nonce and hash examples were exercised in Chromium: approved inline scripts ran and unapproved scripts were blocked. Other browsers, strict-dynamic dependency loading, and report delivery still need deployment-specific testing; no production security assessment is implied.
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan