Broken Access Control in Sails with Api Keys
Broken Access Control in Sails with Api Keys — how this specific combination creates or exposes the vulnerability
Broken Access Control occurs when an API fails to enforce proper authorization checks, allowing one user to access or modify another user’s resources. In Sails.js, this risk is heightened when API keys are used for authentication but not accompanied by robust authorization logic. An API key may identify a service or application, but if the controller actions do not verify that the requesting key is authorized to access a specific record (e.g., user data scoped by tenant or role), the endpoint becomes vulnerable to BOLA/IDOR-style attacks.
For example, a Sails API route like /user/:userId/profile might validate the presence of an API key in a header but skip checking whether that key is permitted to view the profile for the supplied userId. Because Sails encourages rapid development with blueprint routes and implicit model bindings, developers might inadvertently expose model instances without scoping the query to the authenticated key’s permissions. Attackers can iterate numeric IDs or guess UUIDs and observe whether the response differs based on authorization context, leading to unauthorized data exposure.
Another scenario involves role-based access where an API key with elevated scopes is used across endpoints without validating scope per request. If middleware sets a broad req.apiKey object containing roles and permissions but controllers do not revalidate those permissions for each sensitive action, an attacker could exploit horizontal privilege escalation. This is particularly dangerous in microservice-style integrations where many services share a common API key for convenience but the backend does not enforce least privilege at the endpoint level.
The interplay between Sails’ ORM-style query building and API key usage can also lead to insecure direct object references. A controller might write User.findOne(req.param('id')) without ensuring that the req.apiKey is associated with the same tenant or organizational boundary as the requested user. Without explicit scoping, the ORM may return a record that exists in the database but should be invisible to the caller.
Api Keys-Specific Remediation in Sails — concrete code fixes
To mitigate Broken Access Control when using API keys in Sails, ensure every controller action explicitly validates the relationship between the key and the requested resource. Avoid relying on blueprint routes for sensitive endpoints; instead, implement explicit actions with clear authorization checks. Below are concrete code examples demonstrating secure patterns.
Example 1: Scoped lookup with API key ownership
Assume an API key is linked to an organization, and users belong to organizations. The controller should resolve the user in the context of that organization rather than trusting the ID alone.
// api/controllers/ProfileController.js
module.exports = {
async getProfile(req, res) {
const apiKey = req.apiKey; // injected by auth middleware
if (!apiKey) {
return res.unauthorized('Missing API key');
}
const userId = req.param('userId');
if (!userId) {
return res.badRequest('userId is required');
}
// Ensure the user belongs to the same organization as the API key
const profile = await User.findOne({
id: userId,
organizationId: apiKey.organizationId
});
if (!profile) {
return res.notFound('Profile not found or access denied');
}
return res.ok(profile);
}
};
Example 2: Scope validation for shared API keys
When an API key is shared across services, validate scope and resource ownership explicitly. Use policies to centralize these checks.
// api/policies/check-scope.js
module.exports = async function checkScope(req, res, next) {
const apiKey = req.apiKey;
const requiredScope = req.options.scope; // defined in route config
if (!apiKey.scopes.includes(requiredScope)) {
return res.forbidden('Insufficient scope for this operation');
}
// Additional tenant/resource check if needed
if (req.options.requiresResourceOwnership) {
const resourceId = req.param('resourceId');
const hasAccess = await ApiKeyResource.exists({
apiKeyId: apiKey.id,
resourceId
});
if (!hasAccess) {
return res.forbidden('Not authorized for this resource');
}
}
return next();
};
// config/policies.js
module.exports.policies = {
ProfileController: {
'*': 'check-scope',
getProfile: { scope: 'profile:read', requiresResourceOwnership: true }
}
};
Example 3: Secure blueprint alternative with explicit middleware
If using blueprint routes, wrap them with custom middleware to enforce scoping before exposing data.
// config/routes.js
module.exports.routes = {
'GET /user/:id/profile': {
policy: 'check-scope',
controller: 'UserController.profile'
}
};
// api/controllers/UserController.js
module.exports = {
async profile(req, res) {
const userId = req.param('id');
const apiKey = req.apiKey;
const profile = await User.findOne(userId).where({
organizationId: apiKey.organizationId
});
if (!profile) {
return res.unauthorized();
}
return res.ok(profile);
}
};
General recommendations
- Always scope queries by the API key’s organization or tenant context.
- Do not expose internal IDs directly; consider using opaque identifiers where feasible.
- Centralize authorization logic in policies or services to avoid duplication and drift.
- Audit controller actions periodically to ensure no blueprint routes bypass authorization.