Strapi CMS Security: JWT Forgery, Plugin Vulnerabilities, and the Default Admin Problem
Strapi is the most popular open-source headless CMS, with tens of thousands of production deployments. It's also a recurring finding on our platform audits. JWT secrets that can be guessed, plugin vulnerabilities that haven't been patched, admin panels exposed to the internet, and role permissions that commonly grant too much. A deep dive into the Strapi attack patterns and hardening.
Founder of Valtik Studios. Pentester. Based in Connecticut, serving US mid-market.
Why Strapi keeps appearing on audits
Here's the part consultants don't put in the glossy PDF.
Strapi bills itself as the leading open-source headless CMS. Self-hosted, Node.js-based, SQL database (SQLite, Postgres, MySQL). Customizable via plugins. Admin panel for content management. REST and GraphQL APIs auto-generated from content types.
Adoption is huge. The GitHub repo has 60,000+ stars and Strapi deployments power content for thousands of websites, mobile apps, and B2B applications. It's particularly popular in European markets and among agencies building for SMB clients.
It's also consistently misconfigured. The default setup requires thoughtful hardening that many deployments skip. The plugin ecosystem. A core feature. Introduces variable-quality third-party code. The JWT-based authentication has specific risks. And self-hosted deployments frequently lag on updates, accumulating known CVEs.
This post walks through the specific Strapi attack patterns we find on penetration tests, the underlying misconfigurations. And the hardening every production Strapi deployment needs.
Attack pattern 1: Admin panel exposed to the internet
Strapi's admin panel lives at /admin. Full content management, user management, role editing, plugin configuration. All accessible through this interface after authentication.
The misconfiguration: developers run Strapi and expose the entire application (including /admin) to the public internet, relying on admin credentials to protect sensitive operations.
Enumeration is trivial. The /admin path returns a Strapi-branded login page that's easily identified. Shodan queries for "strapi" in HTTP bodies find thousands of exposed admin panels.
Attack:
- Find exposed Strapi instance at
target.example.com/admin - Enumerate Strapi version from login page (via source code, embedded scripts)
- Check for known CVEs affecting that version
- Attempt default credentials (if fresh deployment not yet configured)
- Brute-force credentials (Strapi has rate limiting but it's not aggressive)
Real finding: a company's Strapi admin panel was at api.example.com/admin. Their admin account used the email address of the company's founder and a password that matched her LinkedIn-scrapeable data. Credential-stuffing test took 10 minutes. Full content access followed.
The fix:
- Never expose the
/adminpath to the public internet. Reverse proxy it behind VPN, IP allowlist, or authenticated proxy.
# nginx configuration
location /admin {
# IP allowlist for admin panel
allow 203.0.113.0/24; # office VPN range
deny all;
proxy_pass http://strapi_backend;
}
Location /api {
# Public API access
proxy_pass http://strapi_backend;
}
Location /uploads {
# Public file access (if using local provider)
proxy_pass http://strapi_backend;
}
- Strong admin credentials via password manager
- MFA on admin accounts. Strapi supports TOTP-based MFA via the
users-permissionsplugin or Strapi Cloud
Attack pattern 2: JWT secret exposure / weak generation
Strapi uses JWTs for authentication. The admin panel and API both generate JWT tokens signed with a server-side secret.
Common vulnerabilities:
JWT secret not configured
Strapi generates a JWT secret during initial setup. If the environment variable JWT_SECRET (for admin) or API_TOKEN_SALT (for API tokens) isn't set, Strapi generates a default or uses a predictable fallback in older versions.
JWT secret in public repository
Developers commit Strapi config files containing secrets:
// config/server.js
module.exports = ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
app: {
keys: env.array('APP_KEYS', ['key1', 'key2', 'key3', 'key4']), // default keys committed
},
});
Or in environment files:
#.env committed to public repo
JWT_SECRET=mySecretKey123
API_TOKEN_SALT=abcdef1234567890
Weak JWT secret
A secret like admin or strapi123 is trivially crackable. Attackers who can obtain a legitimate JWT can run offline brute-force on the signature until they find the secret.
JWT secret sharing across environments
Development and production Strapi instances sharing the same JWT secret means development-environment compromise affects production.
Attack:
# Obtain a JWT from a legitimate session or account
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# Crack the JWT secret offline
hashcat -m 16500 jwt.txt wordlist.txt
# Once cracked, forge admin JWTs
python3 << 'EOF'
import jwt
import time
Secret = "cracked_secret_here"
payload = {
'id': 1, # admin user ID
'isAdmin': True,
'iat': int(time.time()),
'exp': int(time.time()) + 3600 * 24 * 365, # 1 year
}
token = jwt.encode(payload, secret, algorithm='HS256')
print(token)
EOF
Forged token with admin privileges. Full Strapi compromise.
The fix:
- Generate strong JWT secrets. At least 32 random bytes:
openssl rand -hex 32
- Different secrets per environment. Never share between dev and prod
- Secrets in environment variables. Never in source code
- Rotate secrets periodically. Every 6-12 months minimum
- Audit git history for committed secrets (rotate anything that was ever exposed)
Attack pattern 3: Role permission over-provisioning
Strapi has a role-based permission model. Default roles:
- Super Admin. Full access
- Editor. Content management
- Author. Create/edit own content
- Authenticated. API role for logged-in users (not admin)
- Public. API role for unauthenticated users
Each role has granular permissions per content type and per action (find, findOne, create, update, delete, etc.).
Common misconfigurations:
- Public role with broader permissions than intended. Often during development, widened for testing and not tightened
- Authenticated role allows any authenticated user to modify any content. No ownership check
- Custom roles inheriting more than intended from parent roles (Strapi's role model isn't strictly hierarchical, but assumptions can drift)
- API permissions don't match UI permissions. The admin UI appropriately shows restricted options to limited roles, but the API exposes the same data without the UI constraints
Real finding: a marketing agency's Strapi powered multiple client websites. The Public role had find permissions on the User collection. Any unauthenticated request returned the email and username of every user. Both admin users and customer accounts across all clients. Single-request data breach affecting 50+ clients.
The fix:
- Audit Public role carefully. Should have
findandfindOneonly on truly public content types (blog posts, pages). Never on User, AdminUser, or admin-adjacent collections. - Audit Authenticated role. Appropriate access for logged-in users, not implicit "logged-in = can do anything"
- Test role permissions via API calls. Don't rely on admin UI behavior
- Use custom roles for complex permission models instead of stretching default roles
Attack pattern 4: Plugin vulnerabilities and dependencies
Strapi's plugin ecosystem is a core value proposition and a persistent security concern. Plugins include:
- Official Strapi plugins. Generally well-maintained but still have had CVEs
- Community plugins. Variable quality
- Custom plugins. Written by the organization itself
Each plugin runs with Strapi's privileges. Plugin vulnerabilities translate directly to Strapi compromise.
Common plugin security issues:
- Plugins with known CVEs that haven't been updated
- Plugins that accept untrusted user input without validation
- Plugins that expose administrative endpoints without appropriate permission checks
- Plugins that log sensitive data
- Plugins with their own dependency vulnerabilities
Real finding: a customer used a community-authored plugin for image manipulation. The plugin shelled out to ImageMagick with user-provided parameters → RCE via crafted filename. The plugin hadn't been updated in 18 months despite ImageMagick CVEs during that period.
The fix:
- Inventory all plugins. Official, community, custom
- Keep plugins updated. Quarterly review minimum
- Security review of community plugins before use
- Dependency scanning.
npm auditon Strapi projects catches dependency-level CVEs - Minimize plugin usage. Each plugin is attack surface
- Consider removing unused plugins even if they're installed but disabled
Attack pattern 5: API token over-privileging
Strapi supports API tokens for server-to-server authentication. Tokens can be:
- Read-only. Can only read content
- Full access. Full admin-equivalent access
- Custom. Specific permissions
Common misconfigurations:
- Full-access tokens used for frontend rendering. The token is in client-side code, any visitor can copy it and make admin-level API calls
- Tokens without expiration. Stay valid indefinitely
- Shared tokens across integrations. Single compromise affects multiple systems
- Tokens in git history. Leaked and not rotated
Real finding: an e-commerce site used a Strapi "Full Access" token for its Next.js frontend to fetch product data. The token was in NEXT_PUBLIC_STRAPI_TOKEN, meaning it was bundled into every page served to visitors. Attackers extracted the token within minutes of visiting the homepage → full admin access to the Strapi backend, which was used to modify product prices, create fake products. And delete legitimate content.
The fix:
- Never use full-access tokens in client-side code
- Use read-only or custom tokens for frontend purposes
- Set token expiration where possible
- Per-integration tokens with minimum necessary permissions
- Token rotation on schedule and on staff changes
Attack pattern 6: CORS misconfiguration
Strapi has configurable CORS. Common mistakes:
- CORS configured to allow any origin (
*) - CORS allowlist includes localhost or development origins in production
- CORS allows credentials with wildcard origin ( blocked by browsers but some servers incorrectly accept)
- CORS allowlist includes dead subdomains that could be taken over
The fix:
// config/middlewares.js
module.exports = [
//...
{
name: 'strapi::cors',
config: {
origin: ['https://www.example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD'],
headers: ['Content-Type', 'Authorization'],
credentials: true,
},
},
//...
];
Production CORS should have:
- Specific origins only
- No wildcards
- No localhost in production
- Regular audit of allowed origins
Attack pattern 7: File upload vulnerabilities
Strapi has built-in file upload via the upload plugin. Common issues:
- Upload provider misconfigured (local filesystem writable by wrong user)
- S3 / cloud provider credentials leaked via config
- Upload size limits not enforced
- File type validation bypasses (magic bytes vs extension mismatch)
- Uploaded files serve with execution permissions (
.php,.jsin upload directories)
Attack:
- Upload a malicious file disguised as image
- Trigger execution via path traversal or direct request
- RCE via uploaded file
The fix:
- Validate file types by content inspection, not extension
- Store uploaded files in cloud storage (S3, GCS) with appropriate access controls
- Never serve upload directories with script execution enabled
- Limit upload sizes appropriately
- Scan uploads for malicious content where possible
Attack pattern 8: GraphQL introspection and query complexity
If you use the GraphQL plugin, you get similar issues to any GraphQL server:
- Introspection enabled in production → full schema disclosure
- No query depth limit → DoS via deeply nested queries
- No query complexity scoring → expensive query DoS
- Field-level permissions not enforced → data exposure via clever queries
The fix:
// config/plugins.js
module.exports = {
graphql: {
config: {
endpoint: '/graphql',
shadowCRUD: true,
playgroundAlways: false, // disable playground in production
depthLimit: 7, // limit query depth
amountLimit: 100, // limit returned amount
apolloServer: {
tracing: false,
introspection: false, // disable introspection in production
},
},
},
};
Attack pattern 9: Outdated Strapi versions
Strapi's release cadence has been fast. Strapi 5 released in late 2024. Strapi 4 is still supported but gets fewer updates. Strapi 3 is EOL.
Common issue: self-hosted Strapi deployments running versions 2-3 years old with known CVEs.
The fix:
- Strapi 5 for new deployments
- Migration plan for Strapi 4 → 5 (breaking changes exist)
- Strapi 3 must be migrated or replaced. No longer receives security updates
- Subscribe to Strapi security advisories
- Quarterly update cadence
Attack pattern 10: Database exposure
Self-hosted Strapi connects to a database (PostgreSQL, MySQL, or SQLite). Common database-level issues:
- Database port exposed to public internet
- Database credentials in config files committed to git
- Database backups stored publicly (S3 bucket misconfigurations)
- SQLite database file accessible via web (if stored in public directory)
- Shared database credentials between environments
The fix:
- Database on private network only
- Database credentials in environment variables
- Backups with encryption at rest and appropriate access controls
- Never store SQLite database file in publicly-served directory
The hardening checklist
For any production Strapi deployment:
Infrastructure
- [ ] Admin panel
/adminnot exposed to public internet - [ ] Reverse proxy with TLS in front of Strapi
- [ ] Database on private network
- [ ] Regular backups with tested restoration
Authentication
- [ ] Strong JWT secrets (32+ random bytes)
- [ ] Different secrets per environment
- [ ] MFA on admin accounts
- [ ] Strong admin credentials via password manager
- [ ] Regular admin access review
Roles and permissions
- [ ] Public role reviewed and minimized
- [ ] Authenticated role reviewed
- [ ] Custom roles documented with intended permissions
- [ ] Permissions tested via API calls, not UI
API tokens
- [ ] No full-access tokens in client-side code
- [ ] Tokens have expiration where possible
- [ ] Per-integration tokens with minimum permissions
- [ ] Regular token rotation
Plugins
- [ ] All plugins inventoried
- [ ] Plugins updated quarterly minimum
- [ ] Community plugins reviewed before use
- [ ] Dependency scanning (npm audit) in CI
Configuration
- [ ] CORS specifically configured for production origins
- [ ] GraphQL introspection disabled in production
- [ ] File upload validation implemented
- [ ] CSP and security headers configured
Operations
- [ ] Strapi version current (quarterly updates)
- [ ] Monitoring of admin panel access
- [ ] Audit logging enabled
- [ ] Incident response plan
Strapi-specific hardening tips
Use Strapi Cloud for small teams
If you're a small team running Strapi for client work, Strapi Cloud (managed hosting by the Strapi team) handles infrastructure-level security. More expensive than self-hosting but reduces operational burden.
For agencies managing multiple client sites
- Separate Strapi instance per client (not shared)
- Per-client admin credentials
- Automated security updates
- Standardized hardening template for new deployments
For enterprises
- Integration with enterprise SSO (Strapi Enterprise plan)
- Audit log forwarding to SIEM
- Formal change management for content type modifications
- Annual penetration testing
For custom plugin development
- Security review in development lifecycle
- Input validation at plugin boundaries
- Minimum-privilege operation
- Dependency management and scanning
For Valtik clients
Valtik's CMS security audits include Strapi-specific reviews:
- Admin panel exposure and authentication review
- JWT secret strength and handling audit
- Role and permission matrix audit
- Plugin inventory and security assessment
- API token inventory and exposure check
- Configuration review (CORS, GraphQL, CSP)
- Plugin code review for custom plugins
- Database and infrastructure security
If you run Strapi in production and haven't had a security review, reach out via https://valtikstudios.com.
The honest summary
Strapi is a capable platform with specific attack surfaces that require attention. The patterns in this post. Exposed admin, weak JWT secrets, over-permissive roles, over-privileged tokens, unpatched plugins. Appear in every unaudited Strapi deployment.
The hardening is tractable. It requires explicit attention to configuration than relying on defaults. Audit yours before it becomes the case study.
Sources
Want us to check your Strapi setup?
Our scanner detects this exact misconfiguration. plus dozens more across 38 platforms. Free website check available, no commitment required.
