Stack guide
nginx CSP recipe — a starter Content-Security-Policy that doesn't break your app
The reason teams skip CSP isn't ignorance — it's that the first attempt usually breaks the homepage. The fix is simple: ship CSP in report-only mode for a week, watch the violations land in a reporting endpoint, then enforce. This guide gives you the exact nginx blocks for both phases plus the report-handler logic.
Phase 1 — Report-only
Add `Content-Security-Policy-Report-Only` first. Browsers obey nothing — they just send a JSON POST to `report-uri` (or `report-to` for Reporting API endpoints) when something would have been blocked. Run this for at least 5–7 days, ideally including a release cycle so you catch route-specific scripts.
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
Phase 2 — Enforced
After triaging the report stream, swap the header name to `Content-Security-Policy`. Keep `report-uri` so you continue catching regressions. Remove `unsafe-inline` and wildcards as you go — those are exactly what attackers exploit when an injection bug lands.
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
Other headers to add at the same nginx layer
While you're editing the same nginx block, add the rest of the baseline: HSTS, X-Frame-Options (or CSP `frame-ancestors`), X-Content-Type-Options, and Referrer-Policy. One round-trip to fix the entire `/headers` category in Scorifya.
HSTS missing — nginx (server / location) (nginx)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # If TLS terminates at a CDN, set the header there instead.
HSTS missing — Express (helmet) (typescript)
import helmet from "helmet";
app.use(helmet.hsts({ maxAge: 31_536_000, includeSubDomains: true }));HSTS missing — Apache (vhost) (apache)
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options missing — nginx (nginx)
add_header X-Frame-Options "DENY" always; # Use SAMEORIGIN instead of DENY only if same-origin framing is required.
X-Frame-Options missing — Express (helmet) (typescript)
import helmet from "helmet";
app.use(helmet.frameguard({ action: "deny" }));X-Frame-Options missing — Apache (apache)
Header set X-Frame-Options "DENY"
X-Content-Type-Options missing — nginx (nginx)
add_header X-Content-Type-Options "nosniff" always;
X-Content-Type-Options missing — Express (helmet) (typescript)
import helmet from "helmet"; app.use(helmet.noSniff());
X-Content-Type-Options missing — Apache (apache)
Header set X-Content-Type-Options "nosniff"
Related Scorifya checks: missing_hsts, missing_xfo, missing_xcto, missing_referrer_policy
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.