Broken Access Control in Sails
How Broken Access Control Manifests in Sails
Broken Access Control in Sails.js applications typically emerges from three core architectural patterns that are common in Node.js frameworks but particularly problematic in Sails's model-driven design.
The most prevalent issue is model-level IDOR (Insecure Direct Object Reference). Sails's Waterline ORM makes it trivial to fetch records by ID:
const pet = await Pet.findOne({ id: req.params.id });
Without proper authorization checks, any authenticated user can access any record by simply guessing or incrementing IDs. This becomes especially dangerous when combined with Sails's implicit model routes that automatically generate CRUD endpoints.
Role-based access control bypasses often occur in Sails policies. Developers frequently implement incomplete policy chains:
// policies/isAuthenticated.js
module.exports = async (req, res, next) => {
if (req.session.userId) {
return next();
}
return res.forbidden();
};
This only checks authentication, not authorization. A user authenticated as a regular customer can access admin-only endpoints if the admin policy isn't properly chained.
Property-level authorization bypasses are common in Sails's flexible schema. Developers expose sensitive model properties without considering data exposure:
// api/models/User.js
module.exports = {
attributes: {
email: { type: 'string' },
ssn: { type: 'string' },
isAdmin: { type: 'boolean' }
}
};
Without explicit select or omit clauses, all properties are returned to any user who can access the endpoint.
Dynamic model access through Sails's blueprint routes creates another attack surface. The default blueprint actions (find, findOne, create, update, destroy) are automatically available unless explicitly disabled, allowing unauthorized access to all model operations.
Sails-Specific Detection
Detecting Broken Access Control in Sails applications requires understanding both the framework's conventions and common vulnerability patterns.
Static analysis should focus on controller actions and policies. Look for:
- Direct model queries without authorization checks
- Missing policy chains on sensitive routes
- Blueprint actions that haven't been secured
- Exposed model properties in API responses
Dynamic testing involves attempting to access resources across user boundaries. For a Sails application with user roles (customer, manager, admin), test:
- Access customer records while authenticated as different users
- Access admin endpoints while authenticated as non-admin users
- Access records by incrementing IDs to test for IDOR
- Examine API responses for exposed sensitive properties
middleBrick's Sails-specific scanning automates these detection patterns. The scanner identifies:
- Authentication bypass attempts on model endpoints
- IDOR vulnerabilities through sequential ID testing
- Property exposure by analyzing response schemas vs. expected access levels
- Policy enforcement gaps by testing endpoint accessibility
middleBrick's black-box approach is particularly effective for Sails applications because it tests the actual runtime behavior without requiring source code access. The scanner can detect authorization bypasses that static analysis might miss, such as:
// This appears secure but may have bypasses
async find(req, res) {
const user = await User.findOne(req.session.userId);
if (user.role !== 'admin') {
return res.forbidden();
}
return res.ok(await Pet.find());
}
middleBrick would test this by attempting to access the endpoint with non-admin credentials and verifying the response.
Sails-Specific Remediation
Remediating Broken Access Control in Sails requires a defense-in-depth approach using the framework's built-in features.
Policy-based authorization is Sails's primary security mechanism. Implement granular policies for each access level:
// policies/ownerOrAdmin.js
module.exports = async (req, res, next) => {
const record = await Pet.findOne(req.params.id);
if (!record) return res.notFound();
const user = await User.findOne(req.session.userId);
if (user.id === record.ownerId || user.role === 'admin') {
return next();
}
return res.forbidden();
};
Model-level authorization using lifecycle callbacks prevents unauthorized access at the data layer:
// api/models/Pet.js
module.exports = {
attributes: {
name: { type: 'string' },
ownerId: { model: 'user' }
},
beforeFind: async (values, proceed) => {
const user = await User.findOne(req.session.userId);
if (user.role === 'admin') return proceed();
values.where.ownerId = user.id;
return proceed();
}
};
Blueprint customization is essential for securing auto-generated routes:
// config/blueprints.js
module.exports.blueprints = {
actions: true,
rest: true,
shortcuts: true,
populate: true,
// Disable dangerous defaults
shortcuts: false,
rest: false
};
Property-level filtering ensures sensitive data isn't exposed:
// controllers/PetController.js
async find(req, res) {
const pets = await Pet.find()
.where({ ownerId: req.session.userId })
.omit(['ssn', 'creditCard', 'internalNotes']);
return res.ok(pets);
}
Role-based access control (RBAC) implementation using Sails policies:
// policies/requireRole.js
module.exports = (requiredRole) => {
return async (req, res, next) => {
const user = await User.findOne(req.session.userId);
if (!user) return res.forbidden();
const roleHierarchy = { user: 1, manager: 2, admin: 3 };
if (roleHierarchy[user.role] < roleHierarchy[requiredRole]) {
return res.forbidden();
}
return next();
};
};
Then apply in config/policies.js:
module.exports.policies = {
PetController: {
find: 'requireRole(user)',
create: 'requireRole(user)',
update: 'requireRole(user)',
destroy: 'requireRole(user)',
adminFind: ['requireRole(admin)', 'isAuthenticated']
}
};