Headless Content Management System (CMS) architectures expose API endpoints to multiple frontend origins by design, which creates a larger attack surface than traditional monolithic systems.
The OWASP API risks rank broken object-level authorization and broken authentication as the two most critical API risks, and the DBIR 2025 confirmed 12,195 data breaches across more than 22,000 security incidents. If you're building on Strapi 5, it helps to know what ships out of the box and what still requires plugins or custom middleware.
Strapi 5 introduced meaningful security improvements over v4, including stricter default input validation, mandatory token expiration in refresh-token mode, and tighter registration field controls. But there are still a few gaps to plan for.
This guide covers Strapi 5's built-in security features, the best available authentication plugins, API protection and rate limiting strategies, data validation and sanitization patterns, backup and environment security, and production hardening best practices. You'll also find a quick-reference compatibility table for every security plugin discussed.
In brief:
- Strapi 5 ships with JSON Web Token (JWT) authentication, including refresh-token rotation, Role-Based Access Control (RBAC), and Helmet/CORS middleware built in.
- The Strapi 5 plugin ecosystem still has gaps for MFA, audit logging, and passwordless auth, so you may need custom middleware in a few places.
- CVE-2025-53092 affected all Strapi versions before 5.20.0 with no workaround, so if you're on an earlier version, upgrade as a priority.
- A defense-in-depth approach, pairing Strapi security plugins with infrastructure protections like WAF, TLS 1.3, and reverse proxy hardening, is a good baseline for production.
Strapi 5 Built-In Security Features
Before reaching for third-party plugins, it helps to know what Strapi gives you for free. Strapi 5 ships with a multi-layered security architecture that covers authentication, authorization, and HTTP-level protections.
Users and Permissions, JWT, and Role-Based Access Control
The built-in Users and Permissions plugin (@strapi/plugin-users-permissions) handles user registration, login, and session management. Strapi 5 introduced a refresh-token JWT management mode alongside the legacy-support mode. When refresh-token mode is active, access tokens enforce a mandatory 15-minute expiration that isn't configurable, which helps limit token lifetimes.
module.exports = {
'users-permissions': {
config: {
jwtManagement: 'refresh-token' // or 'legacy-support'
}
}
};Beyond JWT, Strapi 5 supports API tokens for server-to-server authentication with configurable durations (7, 30, 90 days, or unlimited) and scopes (read-only, full access, or custom per-content-type).
The RBAC docs now cover programmatic custom conditions registered during the bootstrap function. Handlers can return a boolean for simple allow or deny decisions, or a sift.js query object for dynamic filtering. This enables content ownership verification, multi-tenant access control, and context-aware permissions without modifying Strapi core code.
One critical Strapi 5 change: register.allowedFields now defaults empty, preventing mass-assignment attacks during registration. You need to explicitly whitelist every field you want users to submit.
Security Middleware: Helmet, CORS, and CSP Configuration
All HTTP-level security is configured in ./config/middlewares.js. The middleware docs cover strapi::security, which integrates Helmet.js for headers like Content Security Policy, X-Content-Type-Options, and X-Frame-Options, and strapi::cors for Cross-Origin Resource Sharing policy.
Both middlewares accept inline configuration:
module.exports = [
'strapi::errors',
'strapi::security',
{
name: 'strapi::cors',
config: {
origin: ['https://app.example.com', 'https://www.example.com'],
credentials: true,
},
},
'strapi::poweredBy',
'strapi::query',
'strapi::body',
'strapi::session',
'strapi::favicon',
'strapi::public',
];Strapi 5 also changed default input validation behavior. Requests that were silently sanitized in v4 now return 400 errors for invalid payloads. That's a useful security improvement, but it also means API clients that sent malformed data without issues in v4 can break after migration.
Authentication and Security Plugins for Strapi 5
Strapi's built-in auth covers the basics, but most production apps need additional authentication methods. Here's what's available for Strapi 5, and what isn't.
Passwordless Authentication (strapi-plugin-passwordless)
The strapi-plugin-passwordless plugin is not compatible with Strapi 5. If passwordless login is on your roadmap, here is the current state:
- The Marketplace no longer lists it.
- The plugin's GitHub repository shows Strapi v3 and v4 support only, with no v5 release planned.
- You'll need a custom implementation for magic-link authentication in Strapi 5.
The core flow is still straightforward: generate a time-limited token, email it to the user, and validate it on callback. You can build that with Strapi's controller docs and a transactional email service. It takes more work than dropping in a plugin, but it gives you full control over token expiration, request throttling, and the user experience.
Social and OAuth Login (Google, GitHub, and More)
Strapi 5's built-in OAuth support is robust and well-documented. The provider docs describe a seven-step authentication flow: your frontend redirects to Strapi, Strapi redirects to the provider, the provider returns an auth code, and Strapi exchanges it for tokens and creates or updates the user record.
For the Google provider, configure the provider in the Admin Panel under Settings > Users & Permissions > Providers with your Client ID, Client Secret, and redirect URLs. The same pattern works for GitHub login and other OAuth 2.0 providers.
One requirement that often trips teams up: set the absolute backend URL in /config/server.js so OAuth callbacks resolve correctly:
module.exports = ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
url: env('PUBLIC_URL', 'http://localhost:1337'),
});For Admin Panel Single Sign-On (SSO) with providers like Okta integration, you need a pricing page Enterprise plan or the SSO add-on. Admin SSO is configured in /config/admin.js using any Passport strategy.
Two-Factor Authentication with HeadLockr
Open-source MFA is not built into Strapi. The HeadLockr docs cover a commercial plugin with verified Strapi 5 compatibility.
Here are the implementation details that matter most:
- It supports five 2FA methods: Time-Based One-Time Password (TOTP) apps, SMS verification, email codes, backup codes, and passkeys via WebAuthn.
- It covers both Admin Panel and Content API authentication flows.
- Installation requires a private registry entry in
.npmrcwith your license key, plus@tanstack/react-query@^5.56.2as a peer dependency. - It injects CSP changes, including
'wasm-unsafe-eval'inscript-src, to support WebAssembly animations.
That CSP review is the main gotcha in an otherwise straightforward setup.
API Protection and Rate Limiting
Unprotected API endpoints invite credential stuffing, data scraping, and denial-of-service attacks. Strapi 5 provides baseline protection, but production deployments usually need more.
Rate Limiting with Custom Middleware
Strapi 5 applies rate limiting by default on authentication and registration endpoints only, not on general API routes. The community rate-limit plugin does not have verified Strapi 5 compatibility, so custom middleware is the more reliable path. Based on OWASP guidance on unrestricted resource consumption, these thresholds are a sensible starting point:
| Endpoint Type | Recommended Limit | Rationale |
|---|---|---|
| Authentication/login | 3-20 requests/min per user | Prevents credential stuffing |
| General API endpoints | 100 requests/15 min per IP | Balances usability and abuse prevention |
| File uploads/complex queries | 10 requests/min per IP | Protects expensive server operations |
A few practical details matter here:
- For multi-instance deployments, in-memory rate limiting breaks down because state is not shared across instances.
- Redis-backed throttling, using atomic
INCRandEXPIRE, is the safer choice for distributed setups. Strapi's Redis throttling walks through one approach. - Place your rate-limiting middleware early in the middleware chain, after error and security middleware but before content handlers.
- Pair rate limiting with authenticated requests so you can identify users by JWT or API token instead of IP address alone.
That combination gives you better abuse control and fewer false positives once traffic grows.
CORS Best Practices and Known Pitfalls
CORS misconfiguration is one of the most common security issues in headless CMS deployments, and it's not theoretical. CVE-2025-53092 affected all Strapi versions before 5.20.0 by reflecting the Origin header without validation. There is no workaround, so upgrading is the path forward.
Three configurations cause most CORS vulnerabilities, based on the OWASP testing guide:
- Wildcard origins on authenticated endpoints (
Access-Control-Allow-Origin: *): acceptable only for fully public, unauthenticated APIs. - Wildcard + credentials: this violates the CORS specification entirely. When
credentials: true, you need explicit origins. - Blind origin reflection: echoing back the
Originheader without validation defeats CORS protections.
Use environment-specific origin allowlists in your Strapi CORS guide setup:
const allowedOrigins = {
development: ['http://localhost:3000', 'http://localhost:4000'],
staging: ['https://staging-app.example.com'],
production: ['https://app.example.com', 'https://www.example.com']
};
module.exports = [
'strapi::errors',
'strapi::security',
{
name: 'strapi::cors',
config: {
origin: allowedOrigins[process.env.NODE_ENV] || [],
credentials: true,
},
},
// ...remaining middleware
];Remember that CORS and CSP are complementary controls. They are not alternatives. An XSS vulnerability in an allowed origin can still exfiltrate data even with a correct CORS configuration.
Data Validation and Sanitization
Validating and sanitizing data that enters your Strapi application protects against injection attacks and maintains data integrity. Strapi 5 strengthened its defaults here, but a known limitation means you should not rely on built-in sanitization alone.
Input Validation (Built-in ORM, Joi, and Yup)
Strapi 5 exposes five core factory functions in controller docs: sanitizeQuery, sanitizeInput, sanitizeOutput, validateQuery, and validateInput. These functions automatically inherit sanitization settings from the content-type schema. Fields marked private are excluded from responses without any manual configuration.
For validation beyond schema-level checks, Strapi is library-agnostic. Use Yup in lifecycle hooks for business logic validation that applies to all create and update operations regardless of the entry point:
// src/api/article/content-types/article/lifecycles.js
const yup = require('yup');
const articleSchema = yup.object().shape({
title: yup.string().required().min(5).max(100),
content: yup.string().required().min(50),
});
module.exports = {
async beforeCreate(event) {
await articleSchema.validate(event.params.data, { abortEarly: false });
},
};Use Joi in middleware for route-specific request validation that runs before controller execution. The OWASP Input Validation Cheat Sheet recommends allowlists over denylists: define exactly what is authorized, and reject everything else.
Sanitization for XSS Prevention (sanitizeQuery, sanitizeInput)
Sanitization removes harmful code from user inputs, protecting against cross-site scripting (XSS) and injection attacks. Strapi's sanitizeQuery and sanitizeInput functions handle this based on your content-type schema, automatically stripping fields that don't belong.
However, there's a critical caveat. Per GitHub Issue #25204, sanitize.input() currently returns input including undeclared fields rather than stripping them. If you assume sanitization removes unknown fields in security-critical flows like user registration or privilege-sensitive updates, you're relying on a false assumption. Pair sanitizeInput with explicit Yup or Joi validation as a second layer.
In practice, the safer architecture looks like this:
- Request-layer middleware validation catches malformed input early.
- Controller-layer Strapi built-ins apply schema-aware validation and sanitization.
- Model-layer lifecycle hooks enforce business rules consistently.
That layered approach is easier to reason about when your API surface grows.
Backup and Environment Security
Data protection and secrets management are foundational to any production Strapi deployment.
Automated Backups with the Backup Plugin
The Backup plugin (strapi-plugin-backup) from ADELABS provides automated backup functionality with confirmed Strapi 5.0.0+ support. Key capabilities include automated backups of uploads and the database to cloud storage, configurable cron schedules for backup runs, and cleanup of old backups.
For Strapi Cloud deployments, managed backups are included. For self-hosted applications, the Backup plugin automates data protection so you are not relying on manual processes.
Environment Variable and Secrets Management
Strapi 5 requires several security-critical environment variables. Keep these in view:
APP_KEYSfor session cookie signingADMIN_JWT_SECRETJWT_SECRETAPI_TOKEN_SALTTRANSFER_TOKEN_SALT
Production environments are better served by a dedicated secrets manager than by committed .env files.
A few details are worth calling out:
- Use a managed secret store such as AWS Secrets Manager, Google Cloud Secret Manager, Azure Key Vault, or HashiCorp Vault for production.
- Any variable prefixed with
STRAPI_ADMIN_is exposed in Admin Panel JavaScript, so never use that prefix for secrets. - Strapi supports zero-downtime key rotation through comma-separated
APP_KEYSvalues. Add a new key first, wait for old sessions to expire, then remove the oldest one. - Rotating
API_TOKEN_SALTis more disruptive because it invalidates all existing API tokens, so coordinate that change with API consumers.
A little care here prevents the kind of secret leakage that usually shows up at the worst possible time.
Production Best Practices for Strapi Security Plugins
Plugins and configuration handle part of the picture. Production security works best as a defense-in-depth approach that extends beyond Strapi itself.
Keep Strapi and Dependencies Updated (npm audit, CVE Monitoring)
Dependency hygiene is one of those tasks teams skip until a bad week forces the issue. A practical baseline looks like this:
- Run
npm audit --audit-level=moderatein your CI pipeline. - Use
npm ci --ignore-scriptsfor installation. - Monitor the NIST National Vulnerability Database for
strapiand@strapipackages. - Track Node.js releases for runtime-level security fixes.
- Watch the Strapi security blog for CMS-specific advisories.
These checks are low effort, and they save you from learning about dependency issues after production starts failing.
The --ignore-scripts flag matters for a reason. A Krebs report covered a self-replicating worm that infected 180+ npm packages in September 2025 and propagated via postinstall scripts. That same month, 18 more packages were compromised through phished maintainer accounts.
Test in Staging Before Deploying Plugins
Never deploy security plugins directly to production. Plugins like HeadLockr modify your CSP at runtime. Others may conflict with custom middleware.
A safer rollout process looks like this:
- Test every plugin in a staging environment that mirrors production.
- Audit security settings after installation.
- Validate that existing authentication flows still work.
- Review your security configuration as part of each release.
That extra pass in staging catches the kind of breakage most teams otherwise find in production.
Defense in Depth: Pair Plugins with Infrastructure Security (WAF, TLS, Reverse Proxy)
Strapi security plugins are one layer. Production deployments also benefit from infrastructure-level protections.
A practical baseline looks like this:
- Use TLS 1.3 guidance and disable TLS 1.1, TLS 1.0, and all SSL versions.
- Configure your reverse proxy with security headers:
Strict-Transport-Security,X-Content-Type-Options: nosniff,X-Frame-Options: SAMEORIGIN, and a strict Content Security Policy. - Apply stricter proxy-layer rate limits on authentication endpoints than on general API traffic.
- Put a Web Application Firewall at the edge and align rules with the OWASP API risks.
- For container deployments, follow the OWASP Docker security guidance: run as non-root, drop unnecessary capabilities, use minimal base images, and scan images in CI.
If you already have strong app-level controls, these infrastructure layers give you better containment when something slips through.
Putting It All Together: A Layered Approach to Strapi 5 Security
Securing a Strapi 5 application is a layered effort. Start with Strapi's built-in features: JWT authentication with refresh-token rotation, RBAC with custom conditions, and the Helmet/CORS middleware stack. Add authentication plugins where needed, keeping in mind that the Strapi 5 ecosystem still has some real gaps: passwordless auth usually means custom code, and MFA currently points you toward a commercial option like HeadLockr.
Protect your APIs with custom rate-limiting middleware, ideally Redis-backed for multi-instance deployments, and keep your CORS origin allowlist tight. Layer Yup or Joi validation on top of Strapi's built-in sanitization, especially given the known limitation with sanitize.input(). Finally, add infrastructure defenses like TLS 1.3, a WAF, reverse proxy hardening, and automated dependency scanning.
Browse the Marketplace for compatible plugins, review the security checklist for any remaining gaps, and check the self-hosted plans if you need enterprise security features or Admin Panel SSO.