Strapi Security Best Practices: API, CORS, and Headers
Strapi security is a topic that deserves more attention than it typically gets in deployment tutorials. Strapi's default configuration is optimized for getting a working API fast — not for production security. Permissive CORS, no rate limiting, default JWT secrets, and publicly accessible content types are common issues in Strapi deployments that make it onto the internet before being properly hardened.
Check your site's security right now: Free ZeriFlow scan →
1. Environment Variable Management
Strapi uses a .env file for all sensitive configuration. The stakes are high: this file contains your database connection string, JWT secrets, and API keys.
Non-negotiable `.env` hygiene rules:
- Add
.envto.gitignoreimmediately when creating a Strapi project. Verify withgit check-ignore -v .envbefore your first commit. - Use a strong, randomly generated
APP_KEYSvalue (Strapi uses this for session cookies and other internal signing):
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
- Similarly generate random values for
API_TOKEN_SALT,ADMIN_JWT_SECRET,TRANSFER_TOKEN_SALT, andJWT_SECRET. - In production, use your hosting platform's secrets management (AWS Secrets Manager, Railway secrets, Heroku config vars, Render environment variables) rather than a
.envfile on disk.
Never use the example values from Strapi's documentation in production. They're in the documentation and therefore known to attackers.
# .env — generate these values, never use defaults
APP_KEYS=<base64_random>,<base64_random>
API_TOKEN_SALT=<base64_random>
ADMIN_JWT_SECRET=<hex_random>
JWT_SECRET=<base64_random>
TRANSFER_TOKEN_SALT=<base64_random>2. Restrictive CORS Configuration
Strapi's default CORS configuration allows all origins (*) in development. In production, this must be changed.
Configure CORS in config/middlewares.ts:
export default [
'strapi::logger',
'strapi::errors',
{
name: 'strapi::security',
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
'connect-src': ["'self'", 'https:'],
'img-src': ["'self'", 'data:', 'blob:', 'https://market-assets.strapi.io'],
'media-src': ["'self'", 'data:', 'blob:'],
upgradeInsecureRequests: null,
},
},
},
},
{
name: 'strapi::cors',
config: {
enabled: true,
headers: '*',
origin: ['https://yourfrontend.com', 'https://www.yourfrontend.com'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
credentials: true,
},
},
'strapi::poweredBy',
'strapi::query',
'strapi::body',
'strapi::session',
'strapi::favicon',
'strapi::public',
];Run a free ZeriFlow scan → on your public Strapi endpoint to check which security headers are present and whether your CORS configuration is leaking allowed origins in responses.
Multiple environments: Use Strapi's environment-specific config files (config/env/production/middlewares.ts) to have strict CORS in production while allowing localhost in development.
3. Rate Limiting
By default, Strapi has no rate limiting. An unprotected Strapi API is vulnerable to brute-force attacks against the authentication endpoints and credential stuffing against user accounts.
Install the rate limiting middleware:
Strapi doesn't include rate limiting out of the box. Add it via the koa2-ratelimit or strapi-plugin-rate-limiter approach:
npm install koa2-ratelimitConfigure in src/middlewares/ratelimit.ts:
import ratelimit from 'koa2-ratelimit';
export default (config, { strapi }) => {
return ratelimit.middleware({
...ratelimit.RateLimit.middleware({
interval: { min: 15 },
max: 100,
prefixKey: ({ ctx }) => `${ctx.request.ip}`,
}),
...config,
});
};Register it in config/middlewares.ts and apply stricter limits to auth endpoints:
// In a custom auth rate limiting middleware
// Target: /api/auth/local and /api/auth/local/register
// Limit: 5 attempts per 15 minutes per IPAt the infrastructure level (Nginx, Cloudflare), add rate limiting before requests reach Strapi entirely.
4. Helmet.js Security Headers
Strapi uses the strapi::security middleware (wrapping koa-helmet) to add security headers. By default, several key headers are set, but the configuration benefits from explicit tuning.
The strapi::security middleware in config/middlewares.ts corresponds to Helmet.js settings. The defaults include:
X-DNS-Prefetch-ControlX-Frame-Options: SAMEORIGINStrict-Transport-Security(if HTTPS detected)X-Content-Type-Options: nosniffX-XSS-Protection: 0(disables legacy XSS filter)Content-Security-Policy(Strapi admin panel specific)
The CSP defaults are configured for the Strapi admin panel UI — they allow the admin panel's assets to load. Your API endpoint responses typically don't need a CSP header (they return JSON), but responses that include rendered HTML do.
For production, verify headers are being set by running a ZeriFlow scan. Pay particular attention to HSTS — it requires HTTPS to be correctly configured in your proxy layer for Strapi to detect and set it.
5. Admin JWT Secret and Users-Permissions Security
Admin JWT Secret:
The ADMIN_JWT_SECRET signs JWT tokens for Strapi Admin panel sessions. If this secret is weak or known (e.g., the documentation example), an attacker who obtains a valid token can forge tokens for any admin account.
Use a minimum 256-bit random secret:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Rotate this secret if you suspect it has been exposed. Rotation invalidates all existing admin sessions, so coordinate with your team.
Users-Permissions plugin:
The users-permissions plugin handles end-user registration and authentication for your API. Key settings:
- Disable public registration if your API is not supposed to accept self-registration (
/api/auth/local/register). In the Strapi admin panel: Settings → Users & Permissions Plugin → Advanced Settings → Enable email confirmation and consider disabling the register endpoint entirely via a custom middleware if not needed. - Email confirmation: Enable it to prevent fake accounts.
- JWT expiry: Default is 30 days — consider shortening for sensitive applications.
6. Locking Down the Public API: No Public Access by Default
Every Strapi content type has a Permissions matrix in Settings → Users & Permissions Plugin → Roles → Public. By default in older Strapi versions, some content types might have public find and findOne access enabled.
Review and restrict public role permissions:
- 1Go to Settings → Users & Permissions Plugin → Roles → Public.
- 2For every content type, disable all permissions that the public (unauthenticated) API should not have.
- 3Enable only the minimum permissions needed for your public-facing API.
API tokens:
For headless CMS use cases where your frontend fetches content via the Strapi API, use API tokens (Settings → API Tokens → Create new API token) instead of exposing the admin credentials. Create scoped read-only tokens for your frontend and full-access tokens only where needed.
Set token expiry dates — non-expiring tokens are a risk if exposed.
Route-level protection:
For custom controller routes, use the auth: { scope: [...] } configuration or add authentication middleware to individual routes:
// src/api/article/routes/article.ts
export default {
routes: [
{
method: 'GET',
path: '/articles',
handler: 'article.find',
config: {
auth: false, // Public
},
},
{
method: 'POST',
path: '/articles',
handler: 'article.create',
config: {
auth: {
scope: ['api::article.article.create'],
},
},
},
],
};FAQ
### Q: Is it safe to expose a Strapi API publicly on the internet? A: Yes, but only after hardening. A default Strapi install with no rate limiting, permissive CORS, and weak secrets should not be publicly exposed. After applying the hardening in this guide — restrictive CORS, rate limiting, locked permissions, strong secrets — a public Strapi API is appropriate for production use.
### Q: How do I check what my Strapi API endpoint is leaking in its response headers? A: Run a ZeriFlow scan on your Strapi API's public URL. It checks for the presence and configuration of all security headers, TLS settings, and cookie flags.
### Q: Should I use Strapi Cloud vs self-hosted for better security? A: Strapi Cloud handles infrastructure security (SSL, DDoS, platform updates) similar to other managed hosting platforms. Self-hosted gives you more control (custom middleware, server hardening) but requires more operational overhead. Either can be made secure — the application-level security (permissions, secrets, CORS) is identical on both.
### Q: What's the default admin password after Strapi installation? A: There is no default password — Strapi requires you to create an admin account during the first-run setup wizard. There is no backdoor or default credential. However, if you deployed Strapi on a public URL without completing setup, the setup wizard is publicly accessible — complete it immediately and then consider restricting the admin path via IP allowlist.
### Q: How do I prevent Strapi from leaking its version in response headers?
A: By default, Strapi includes an X-Powered-By: Strapi header. Remove it by configuring the poweredBy middleware: in config/middlewares.ts, replace 'strapi::poweredBy' with { name: 'strapi::poweredBy', config: { poweredBy: false } }.
Conclusion
Strapi is an excellent headless CMS, but its out-of-the-box configuration leaves several critical security gaps for production deployments. Strong, unique secrets across all environment variables, restrictive CORS to known origins, rate limiting on authentication endpoints, reviewed public API permissions, and proper security headers via the middleware configuration together create a production-ready security posture.
Validate your Strapi endpoint's external security footprint with an automated scan.
Run a free ZeriFlow scan → — 60 seconds, no credit card.