Broken Authentication in Strapi
How Broken Authentication Manifests in Strapi
Broken Authentication in Strapi often stems from misconfigured authentication providers and flawed session management. The most common vulnerability occurs when Strapi's default authentication middleware is bypassed or improperly configured.
One critical attack pattern involves exploiting Strapi's JWT token handling. If your Strapi instance runs with autoReload: true in development mode, attackers can sometimes trigger token regeneration through specific API calls, causing session fixation vulnerabilities. This is particularly dangerous when combined with Strapi's default rememberMe configuration, which stores tokens in cookies without proper SameSite restrictions.
Another Strapi-specific issue arises from role-based access control (RBAC) misconfigurations. Strapi's default 'public' role often has broader permissions than intended. When developers create custom authentication flows without properly revoking the public role's API access, attackers can exploit endpoints that should be protected. For example, the /content-manager endpoints might remain accessible even when content-type permissions are restricted in the admin panel.
Strapi's provider system also introduces authentication risks. When using OAuth providers like Google or GitHub, improper validation of the state parameter in the OAuth flow can lead to CSRF attacks. Additionally, if the strapi-plugin-users-permissions configuration doesn't enforce email verification, attackers can register with disposable emails and immediately gain authenticated access.
Session fixation attacks are particularly effective against Strapi when the application doesn't properly invalidate sessions on privilege escalation. An attacker can authenticate with a low-privilege account, obtain a valid session ID, then trick an administrator into using that session ID after the attacker has escalated their privileges within the same session.
Brute force attacks against Strapi's authentication endpoints are effective when rate limiting isn't properly configured. The default /auth/local endpoint doesn't implement rate limiting, allowing attackers to attempt thousands of password combinations until they find valid credentials.
Strapi-Specific Detection
Detecting Broken Authentication in Strapi requires examining both configuration files and runtime behavior. Start by inspecting your config/plugins.js file for authentication provider settings:
module.exports = ({ env }) => ({
'users-permissions': {
jwt: {
secret: env('JWT_SECRET'),
expiresIn: env('JWT_EXPIRES_IN') || '1d',
cookie: {
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax' // Should be 'strict' or 'none' in production
}
},
provider: 'local', // Check if this should be restricted
role: {
default: 'authenticated', // Ensure 'public' isn't default
autoVerify: true // Should be true to prevent unverified access
}
}
});middleBrick's scanner specifically tests Strapi authentication endpoints by attempting to bypass authentication through common vectors. The scanner checks if unauthenticated requests can access protected endpoints, tests for session fixation vulnerabilities, and verifies that JWT tokens are properly validated.
The scanner also examines your Strapi content-type permissions. Even if your API endpoints are protected, the content-type permissions in the admin panel might allow public read access to sensitive data. middleBrick's inventory management check identifies these permission gaps by attempting to access content-type endpoints without authentication.
For OAuth-related vulnerabilities, middleBrick tests the OAuth callback endpoints for state parameter validation and checks if email verification is enforced. The scanner also attempts to register new accounts to verify if the system allows unverified registrations.
middleBrick's rate limiting detection specifically identifies if Strapi's authentication endpoints lack proper throttling. The scanner makes multiple rapid authentication attempts and analyzes the server's response patterns to determine if rate limiting is implemented.
The scanner's LLM security checks are particularly relevant for Strapi instances using AI features. It tests for system prompt leakage in any AI-powered content generation features and checks if AI endpoints properly authenticate users before processing requests.
Strapi-Specific Remediation
Fixing Broken Authentication in Strapi requires both configuration changes and code-level security measures. Start with your config/plugins.js file:
module.exports = ({ env }) => ({
'users-permissions': {
jwt: {
secret: env('JWT_SECRET'),
expiresIn: env('JWT_EXPIRES_IN') || '1h', // Shorter expiration is safer
cookie: {
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict', // Most secure option
httpOnly: true
}
},
provider: 'local',
role: {
default: 'authenticated',
autoVerify: true
},
// Add rate limiting to authentication endpoints
rateLimit: {
max: 5, // Max 5 attempts
windowMs: 900000, // 15 minutes
message: 'Too many authentication attempts'
}
}
});For role-based access control, create a middleware that validates user permissions before allowing API access:
// api/middleware/auth-check.js
module.exports = async (ctx, next) => {
const { method, url } = ctx.request;
// Skip authentication for public endpoints
const publicEndpoints = ['/auth/local', '/auth/local/register'];
if (publicEndpoints.some(endpoint => url.startsWith(endpoint))) {
return next();
}
// Check if user is authenticated
if (!ctx.state.user) {
ctx.status = 401;
ctx.body = { message: 'Please authenticate' };
return;
}
// Check role-based permissions
const { role } = ctx.state.user;
const protectedEndpoints = ['/admin', '/content-manager'];
if (protectedEndpoints.some(endpoint => url.startsWith(endpoint))) {
if (role.name !== 'admin') {
ctx.status = 403;
ctx.body = { message: 'Insufficient permissions' };
return;
}
}
await next();
};
// api/routes.js
module.exports = ({ strapi }) => {
return [
{
method: 'GET',
path: '/protected',
handler: async (ctx) => {
// Your protected endpoint logic
ctx.send({ message: 'Access granted' });
},
config: {
policies: ['api/middleware/auth-check']
}
}
];
};For OAuth security, implement proper state parameter validation:
// api/controllers/auth.js
const crypto = require('crypto');
module.exports = {
async login(ctx) {
const state = crypto.randomBytes(16).toString('hex');
ctx.session.authState = state;
// Redirect to OAuth provider with state parameter
ctx.redirect(`https://provider.com/oauth?state=${state}`);
},
async callback(ctx) {
const { state, code } = ctx.request.body;
// Verify state parameter
if (state !== ctx.session.authState) {
ctx.status = 400;
ctx.body = { message: 'Invalid state parameter' };
return;
}
// Continue with OAuth flow
// ...
}
};Implement session invalidation on privilege escalation:
// services/userService.js
const { sanitizeEntity } = require('strapi-utils');
module.exports = {
async update(ctx) {
const { id } = ctx.params;
const data = ctx.request.body;
// Update user
const entity = await strapi.query('user', 'users-permissions').update(
{ id },
data
);
// Invalidate existing sessions if role changed
if (data.role) {
await strapi.plugins['users-permissions'].services.jwt.invalidateUserSessions(id);
}
return sanitizeEntity(entity, { model: strapi.models['user'] });
}
};For additional security, implement IP-based rate limiting using middleware:
// api/middleware/rate-limit.js
const rateLimit = require('express-rate-limit');
module.exports = async (ctx, next) => {
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
await limiter(ctx, next);
};Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |