Stack guide
Express.js Content-Security-Policy — a CSP that doesn't break your app
CSP is the highest-leverage security header in any Node web app — it converts most XSS bugs from "steal the session" into "do nothing" — but it's also the easiest one to ship in a way that breaks every page. The fix is to ship in report-only mode first, watch the violations, and only enforce after the noise stops. Helmet's `contentSecurityPolicy` middleware makes both phases tractable.
1. Phase 1 — Helmet contentSecurityPolicy in report-only
Pass `reportOnly: true` to `helmet.contentSecurityPolicy()` and a `report-uri` (or `report-to`) directive pointing at an endpoint that just logs the JSON body. Browsers obey nothing — they just POST what *would* have been blocked. Run this for at least one release cycle so you catch route-specific third-party SDKs (analytics, chat widgets, embeds).
CSP missing — nginx (nginx)
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'" always;
CSP missing — Express (helmet) (typescript)
import helmet from "helmet";
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
baseUri: ["'self'"],
formAction: ["'self'"],
},
}),
);CSP missing — Apache (apache)
Header set Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'"
Related Scorifya checks: missing_csp
2. Per-request nonces for inline scripts
If your app needs inline `<script>` tags (most server-rendered apps do), add a middleware that generates a 16-byte random nonce per request, sets it on `res.locals.cspNonce`, and includes `'nonce-${nonce}'` in your `script-src`. Your template engine then renders the nonce attribute on every inline script. This is what lets you remove `'unsafe-inline'` — the single biggest weakness in most CSPs.
Weak CSP — nginx (tighten script-src) (nginx)
# Drop 'unsafe-inline' / wildcards in production; use nonces or hashes for any inline script. add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'" always;
Weak CSP — Express (helmet) (typescript)
import helmet from "helmet";
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
objectSrc: ["'none'"],
},
}),
);Weak CSP — Apache (apache)
# Replace permissive policy with explicit hosts and no unsafe-inline where possible. Header set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'"
Related Scorifya checks: weak_csp
3. Phase 2 — Flip from report-only to enforced
When the report endpoint stops receiving new violation types for a few days, drop the `reportOnly: true` flag. Keep `report-uri` even after enforcing — you want to know immediately if a deploy regresses (a vendor SDK adding a new domain, a marketing team adding an inline tag). Re-scan with Scorifya to confirm the `weak_csp` finding clears.
Weak CSP — nginx (tighten script-src) (nginx)
# Drop 'unsafe-inline' / wildcards in production; use nonces or hashes for any inline script. add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'" always;
Weak CSP — Express (helmet) (typescript)
import helmet from "helmet";
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
objectSrc: ["'none'"],
},
}),
);Weak CSP — Apache (apache)
# Replace permissive policy with explicit hosts and no unsafe-inline where possible. Header set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'"
Related Scorifya checks: weak_csp
Background
What is Content Security Policy (CSP)? A practical explainer →
An accessible explanation of Content Security Policy: what it does, why it exists, the directives that matter, and how to roll one out without breaking your app.
Read more
Verify with a fresh scan
After deploy, run the scanner on the affected hostname. Headers and TLS settings update on the very next request, so you should see the score move within seconds.