Configure security headers and CSP for Strapi + Next.js – defensive guide
Learn step‑by‑step how to add essential security headers and a strict Content‑Security‑Policy to a Strapi headless CMS and Next.js frontend, protecting against XSS, clickjacking and data‑leak risks.
By the Revisionary team · · 4 min read
How do you add security headers and a strict Content‑Security‑Policy to a Strapi + Next.js site?
Add the essential HTTP headers in both the API layer (Strapi) and the presentation layer (Next.js). Use a Content‑Security‑Policy (CSP) that allows only the resources you need, then lock down the rest with Helmet, strict‑transport‑security, X‑Frame‑Options and related headers. The steps below cover configuration, testing and maintenance for a production‑grade defensive setup.
Why security headers matter
Security headers are instructions sent by the server that tell browsers how to handle the response. They mitigate many common attacks without changing application code:
| Header | Primary protection | Typical value |
|---|---|---|
Content‑Security‑Policy | Cross‑site scripting (XSS), data injection, mixed‑content | default-src 'self'; script-src 'self' https://cdn.example.com; object-src 'none'; |
X‑Content‑Type‑Options | MIME‑sniffing | nosniff |
X‑Frame‑Options | Clickjacking | DENY or SAMEORIGIN |
Strict‑Transport‑Security (STS) | Enforces HTTPS | max-age=31536000; includeSubDomains; preload |
Referrer‑Policy | Referrer leakage | strict-origin-when-cross-origin |
Permissions‑Policy | Controls browser features (camera, geolocation…) | geolocation=(), camera=() |
When a headless CMS powers a front‑end framework, you must set these headers in both places: the Strapi API (which serves JSON) and the Next.js server (which serves HTML, JS, CSS, images). Doing so ensures that even if an attacker compromises one layer, the other still enforces the same policies.
Prerequisites
- Strapi 5 installed and running.
- Next.js project that fetches content via Strapi’s REST or GraphQL API.
- Access to the server configuration (Node.js environment) – no need for a reverse proxy, but you can also apply the same headers at the edge (Vercel, Cloudflare) if you prefer.
- Optional: the Revisionary plugin is installed if you want an immutable audit log of header‑related configuration changes (helps with GDPR compliance).
1. Add security headers to Strapi
Strapi does not ship with a built‑in header manager, but you can use the popular helmet middleware. Create a custom middleware file and register it in config/middleware.ts.
// ./src/middleware/securityHeaders.js
import helmet from 'helmet';
export default helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.example.com"],
styleSrc: ["'self'", "'unsafe-inline'"], // adjust if you use inline styles
imgSrc: ["'self'", "data:", "https://images.example.com"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
},
// Additional headers
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
hidePoweredBy: true,
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
frameguard: { action: "deny" },
noSniff: true,
});Now register the middleware:
// ./config/middleware.ts
module.exports = [
'strapi::errors',
'strapi::security',
// other default middlewares …
{
name: 'global::securityHeaders',
// Path is relative to the project root
resolve: './src/middleware/securityHeaders.js',
},
];Key points
- The CSP directives mirror what the front‑end needs. Keep the list as tight as possible.
upgradeInsecureRequestsforces browsers to load HTTP resources over HTTPS, preventing mixed‑content warnings.hidePoweredByremoves theX-Powered-By: Strapiheader, reducing information leakage.
Verify the headers
Start Strapi and request any endpoint, e.g. curl -I http://localhost:1337/api/articles. You should see the full list of headers.
2. Add security headers to Next.js
Next.js allows you to set custom HTTP headers in next.config.js using the headers async function. Combine this with the next-secure-headers package for a concise configuration.
// next.config.js
const { createSecureHeaders } = require('next-secure-headers');
module.exports = {
async headers() {
return [
{
// Apply to all routes
source: '/(.*)',
headers: createSecureHeaders({
contentSecurityPolicy: {
// Same directives as Strapi, but you can add Next‑specific sources
directives: {
defaultSrc: "'self'",
scriptSrc: ["'self'", "https://cdn.example.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https://images.example.com"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: "'none'",
upgradeInsecureRequests: true,
},
},
// Other headers
referrerPolicy: 'strict-origin-when-cross-origin',
permissionsPolicy: {
camera: [],
microphone: [],
geolocation: [],
},
// HSTS – only works on HTTPS production domains
strictTransportSecurity: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
xssProtection: true,
frameGuard: 'deny',
noSniff: true,
}),
},
];
},
};Explanation
createSecureHeadersbuilds the full header set; you can customize any directive.- The CSP here matches the Strapi API’s CSP, ensuring that both layers agree on allowed sources.
- Deploy to a HTTPS‑enabled host (Vercel, Netlify, etc.) for HSTS to take effect.
Testing in the browser
Open the site in Chrome, open DevTools → Network, select the main document, and inspect the Response Headers tab. Verify that every header appears exactly once.
3. Align CSP between API and Front‑end
Because Strapi returns JSON, the CSP on the API does not affect the browser directly. However, a mismatched policy can cause CORS pre‑flight failures when the front‑end tries to fetch resources from the API.
- Keep the
connect-srcdirective in the front‑end CSP in sync with the API origin. - If you use a CDN for static assets, add it to both
script-srcandstyle-src. - When you add a third‑party analytics script, update both CSPs simultaneously.
4. Automate header validation in CI/CD
Add a simple test to your CI pipeline that fails if any required header is missing. Example using Node's fetch and Jest:
// __tests__/securityHeaders.test.js
const fetch = require('node-fetch');
test('API returns required security headers', async () => {
const res = await fetch('http://localhost:1337/api/articles');
const headers = res.headers;
expect(headers.get('content-security-policy')).toBeTruthy();
expect(headers.get('strict-transport-security')).toMatch(/max-age=31536000/);
expect(headers.get('x-frame-options')).toBe('DENY');
expect(headers.get('x-content-type-options')).toBe('nosniff');
});Run this test as part of your GitHub Actions or other CI system. If a header is removed or altered unintentionally, the build fails, preventing a vulnerable release.
5. Ongoing maintenance checklist
| Frequency | Task |
|---|---|
| On each new dependency | Review CSP to add any required script or style source. Update both Strapi and Next.js configs. |
| Quarterly | Run a CSP scanner (e.g., Mozilla Observatory) against the live site. Fix any reported violations. |
| After any infrastructure change | Verify that the strict-transport-security header is still present and that the preload flag matches the domain registration. |
| When GDPR‑related data is stored | Ensure the audit log (Revisionary) records header‑change events. Anonymize IPs if required (/docs/settings/privacy). |
| After a security incident | Rotate any secret tokens (API keys, JWT signing keys) and re‑evaluate CSP for new attack vectors. |
6. Common pitfalls and how to avoid them
- Overly permissive CSP – Using
script-src 'unsafe-inline'defeats XSS protection. Replace inline scripts with external files or use a nonce/hashes if unavoidable. - Missing
upgrade-insecure-requests– Browsers will still block mixed‑content on HTTPS pages, leading to broken UI. Include the directive. - Duplicate headers – If you set headers both in a reverse proxy (e.g., Nginx) and in the application, the browser may see two values and ignore the policy. Choose a single source of truth.
- Forgotten sub‑domains – STS applies only to the domain that serves the header. Add
includeSubDomainsand register the domain for the HSTS preload list if you own sub‑domains. - CSP violation reports ignored – Enable
report-uriorreport-toin the CSP to collect violation reports. Use a lightweight endpoint (e.g., a Strapi collection type) to store them for later analysis.
7. Linking to Revisionary for audit compliance (optional)
If your organization must prove that header configurations have not been tampered with, enable Revisionary’s audit log. Every change to config/middleware.ts or next.config.js should be committed via a CI pipeline that also creates a versioned entry in the CMS. The audit log (/docs/everyday-use/audit) records who changed what and when, helping with GDPR documentation.
Conclusion
Setting security headers and a strict Content‑Security‑Policy in both Strapi and Next.js creates a layered defence against XSS, clickjacking, data‑leakage and mixed‑content attacks. By using Helmet on the API side and next-secure-headers on the front‑end, you get a concise, maintainable configuration. Pair the headers with automated CI checks, regular CSP scans, and an immutable audit log (Revisionary) to stay compliant with GDPR and industry best practices. Implement the steps above today and reduce the attack surface of your headless CMS‑driven web application.
Further reading
- Strapi configuration reference –
/docs/reference/configuration - Content API documentation –
/docs/reference/content-api - Revisionary audit log details –
/docs/everyday-use/audit - OWASP Top 10 hardening guide for Strapi –
/blog/secure-a-strapi-headless-cms-owasp-top-10-checklist-and-hardening-guide
The Revisionary team
Developers who build Revisionary, a version history plugin for Strapi 5. We write about Strapi, web development, AI and application security.
This article was written with the help of AI.