Broken Access Control in Strapi
How Broken Access Control Manifests in Strapi
Broken Access Control in Strapi typically emerges through misconfigured role-based access control (RBAC) and inadequate validation of user permissions. Strapi's dynamic nature, where content types and roles are defined at runtime, creates specific attack vectors that differ from traditional frameworks.
The most common manifestation occurs in content-type permissions. Strapi allows administrators to define granular permissions for each role across create, read, update, and delete (CRUD) operations. However, developers often overlook the "count" and "find" permissions, which can lead to data enumeration attacks. An attacker with "find" permission on a users-permissions role can retrieve all user records, including sensitive fields like email addresses and roles.
Another critical vulnerability arises from improper use of strapi.plugins['users-permissions'].controllers. In Strapi's plugin architecture, the users-permissions plugin handles authentication and authorization. Developers sometimes bypass this by directly accessing controllers or services without proper permission checks. For example:
// Vulnerable code - missing permission check
const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
module.exports = {
async update(ctx) {
const { id } = ctx.params;
const entity = await strapi.services.article.update({ id }, ctx.request.body);
return sanitizeEntity(entity, { model: strapi.models.article });
}
};
This controller allows any authenticated user to update any article if the route is exposed, regardless of whether they own the article or have update permissions.
Strapi's dynamic policy system also introduces risks. Policies are middleware functions that run before controller actions. If a developer creates a custom policy but forgets to apply it to all relevant routes, or if they incorrectly configure the policy to only check for authentication rather than proper authorization, broken access control results.
Cross-component access is another Strapi-specific issue. Since Strapi plugins can register their own controllers and services, an attacker might exploit a vulnerability in one plugin to access data from another plugin. For instance, a vulnerability in the content-manager plugin could potentially allow access to data managed by the users-permissions plugin.
Strapi-Specific Detection
Detecting broken access control in Strapi requires both manual code review and automated scanning. Start by examining your role definitions in the admin panel under "Roles & Permissions." Verify that each role has appropriate permissions for each content type, and pay special attention to the "count" and "find" permissions.
Code review should focus on controller files in your API directories. Look for controllers that don't use strapi.plugins['users-permissions'].controllers or don't explicitly check permissions. Use Strapi's built-in permission checking utilities:
// Secure pattern - explicit permission check
const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
module.exports = {
async update(ctx) {
const { id } = ctx.params;
// Check if user has update permission on this article
const hasPermission = await strapi.plugins['users-permissions'].services.permissions.hasPermission(
ctx,
'update', // action
'application', // plugin
'article' // resource
);
if (!hasPermission) {
return ctx.unauthorized('You are not allowed to perform this action');
}
const entity = await strapi.services.article.update({ id }, ctx.request.body);
return sanitizeEntity(entity, { model: strapi.models.article });
}
};
For automated detection, middleBrick's API security scanner specifically tests Strapi endpoints for broken access control vulnerabilities. The scanner identifies endpoints that allow unauthorized data access, tests for IDOR (Insecure Direct Object Reference) vulnerabilities by manipulating object identifiers in requests, and checks whether authentication is properly enforced across all endpoints.
middleBrick's scanning process for Strapi includes:
- Authentication bypass testing - attempting to access protected endpoints without credentials
- IDOR testing - modifying object IDs in requests to access other users' data
- Permission escalation testing - attempting to perform actions beyond the authenticated user's role
- API endpoint inventory - mapping all accessible endpoints and their authentication requirements
The scanner provides a security score (A-F) and detailed findings with severity levels, helping you prioritize fixes. For Strapi specifically, middleBrick checks for common misconfigurations like exposed admin endpoints, improperly configured CORS policies, and missing rate limiting on authentication endpoints.
Strapi-Specific Remediation
Remediating broken access control in Strapi requires a multi-layered approach. Start with proper role configuration in the admin panel, ensuring each role has only the permissions necessary for its function. Use the principle of least privilege - if a role doesn't need "find" permission on a content type, don't grant it.
In your API code, always use Strapi's permission services for access control checks. Here's a comprehensive pattern for secure controller implementation:
const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
module.exports = {
async find(ctx) {
// Check if user has find permission
const hasPermission = await strapi.plugins['users-permissions'].services.permissions.hasPermission(
ctx,
'find',
'application',
'article'
);
if (!hasPermission) {
return ctx.unauthorized('You are not allowed to view articles');
}
// Apply ownership filter if user only has limited permissions
const params = { ...ctx.params, ...ctx.request.query };
const entities = await strapi.services.article.find(params, ctx.state.user);
return entities.map(entity => sanitizeEntity(entity, { model: strapi.models.article }));
},
async findOne(ctx) {
const { id } = ctx.params;
// Check if user has find permission
const hasPermission = await strapi.plugins['users-permissions'].services.permissions.hasPermission(
ctx,
'find',
'application',
'article'
);
if (!hasPermission) {
return ctx.unauthorized('You are not allowed to view this article');
}
// Additional check for ownership
const entity = await strapi.services.article.findOne({ id }, ctx.state.user);
if (!entity) {
return ctx.notFound('Article not found');
}
// Verify user owns this entity or has elevated permissions
if (entity.created_by.id !== ctx.state.user.id) {
const canAccessAll = await strapi.plugins['users-permissions'].services.permissions.hasPermission(
ctx,
'find',
'application',
'article',
{ all: true }
);
if (!canAccessAll) {
return ctx.unauthorized('You can only access your own articles');
}
}
return sanitizeEntity(entity, { model: strapi.models.article });
},
async create(ctx) {
const hasPermission = await strapi.plugins['users-permissions'].services.permissions.hasPermission(
ctx,
'create',
'application',
'article'
);
if (!hasPermission) {
return ctx.unauthorized('You are not allowed to create articles');
}
const entity = await strapi.services.article.create(ctx.request.body, {
user: ctx.state.user
});
return sanitizeEntity(entity, { model: strapi.models.article });
}
};
For Strapi v4 and later, use the new permission system with policies. Create a custom policy for common permission checks:
// policies/hasPermission.js
module.exports = async (ctx, next, action, resource) => {
const hasPermission = await strapi.plugins['users-permissions'].services.permissions.hasPermission(
ctx,
action,
'application',
resource
);
if (!hasPermission) {
return ctx.unauthorized(`You are not allowed to ${action} ${resource}`);
}
return next();
};
Then apply it in your routes:
// config/routes.json
{
"routes": [
{
"method": "GET",
"path": "/articles",
"handler": "article.find",
"config": {
"policies": ["plugins::users-permissions.hasPermission"]
}
},
{
"method": "GET",
"path": "/articles/:id",
"handler": "article.findOne",
"config": {
"policies": ["plugins::users-permissions.hasPermission"]
}
}
]
}
Implement proper input validation to prevent IDOR attacks. Always validate that the user has rights to access the specific resource they're requesting, not just that they have general permissions for the content type.
Consider implementing content-scoped permissions for complex scenarios. Strapi's permission system allows you to define custom permissions that can check ownership, group membership, or other business rules before granting access.
Frequently Asked Questions
How can I test for broken access control in my Strapi API?
Run middleBrick's security scanner against your Strapi API endpoints. The scanner tests for authentication bypass, IDOR vulnerabilities, and improper permission enforcement. It provides a security score with detailed findings and remediation guidance. For manual testing, use tools like Postman or curl to attempt accessing endpoints with different user roles, modify object IDs in requests to test for IDOR, and verify that unauthenticated users cannot access protected endpoints.
What's the difference between authentication and authorization in Strapi?
Authentication verifies who a user is (typically via JWT tokens in Strapi), while authorization determines what they're allowed to do. Strapi handles authentication through the users-permissions plugin, but authorization is managed through role-based permissions configured in the admin panel. A common mistake is implementing authentication (users must be logged in) but failing to implement proper authorization (checking if they have rights to perform the specific action on the specific resource).