CWE-284 in Sails
How Cwe 284 Manifests in Sails
Cwe 284 (Improper Access Control) in Sails.js applications typically manifests through misconfigured policies, incorrect model permissions, and flawed authorization logic. Sails' flexible policy system, while powerful, can create security gaps when developers misunderstand how policies apply across different routes and actions.
The most common pattern involves developers creating a policy that checks if a user is authenticated, but failing to verify whether that user actually owns or has permission to access the specific resource. For example:
// policies/isAuthenticated.js
module.exports = async (req, res, next) => {
if (req.session.userId) {
return next();
}
return res.forbidden();
};This policy only verifies authentication, not authorization. A user could access /api/users/999 and see another user's data simply by changing the ID in the URL.
Sails' blueprint routes compound this issue. When blueprint actions are enabled, Sails automatically generates RESTful routes for models. If you have a User model and don't properly restrict blueprint actions:
// config/blueprints.js
module.exports.blueprints = {
actions: true,
rest: true,
shortcuts: true
};This exposes find, findOne, create, update, and destroy actions without any access controls. An attacker can iterate through user IDs or access admin endpoints directly.
Another common manifestation occurs with Sails' populate method. Developers often forget to filter associated records:
// api/controllers/UserController.js
async find(req, res) {
const user = await User.findOne(req.session.userId)
.populate('posts');
return res.json(user);
}If the posts association doesn't filter by author, a user could see posts from other users if they know the post IDs.
Dynamic scoping in Sails Waterline ORM can also introduce Cwe-284 vulnerabilities. When using find with dynamic criteria:
const userId = req.param('userId');
const posts = await Post.find({ author: userId });If userId comes from user input without validation, an attacker could access any user's posts by manipulating the parameter.
Sails' policy hierarchy adds another layer of complexity. Policies can be applied at the controller level, action level, or route level, and the order matters. A common mistake is applying a restrictive policy at the controller level but forgetting that blueprint actions might bypass it:
// config/policies.js
module.exports.policies = {
UserController: 'isAuthenticated',
* : 'isAuthenticated' // global policy
};This setup might seem secure, but blueprint routes could still be accessible if not explicitly restricted.
Sails-Specific Detection
Detecting Cwe-284 in Sails applications requires examining both the codebase and runtime behavior. Start with a comprehensive policy audit:
// Check all policies for proper authorization logic
module.exports = {
'*': [ 'isAuthenticated' ],
UserController: {
'find': [ 'isAuthenticated', 'checkOwnership' ],
'update': [ 'isAuthenticated', 'checkOwnership' ],
'destroy': [ 'isAdmin' ]
}
};Look for policies that only check authentication (req.session.userId) without verifying resource ownership or permissions.
Examine blueprint configuration carefully. In config/blueprints.js, ensure that blueprint actions are only enabled where absolutely necessary:
module.exports.blueprints = {
actions: true, // Only enable if you need custom actions
rest: false, // Disable REST blueprint routes
shortcuts: false // Disable shortcuts for better security
};Audit all controller actions for proper authorization checks. Use a static analysis tool or manual review to verify that every data access operation includes ownership verification:
// Vulnerable pattern
async show(req, res) {
const post = await Post.findOne(req.params.id);
return res.json(post); // No ownership check!
}
// Secure pattern
async show(req, res) {
const post = await Post.findOne({
id: req.params.id,
author: req.session.userId
});
if (!post) return res.notFound();
return res.json(post);
}middleBrick's API security scanner can detect these vulnerabilities by testing authenticated endpoints with different user contexts. The scanner will attempt to access resources with IDs that don't belong to the authenticated user, identifying missing authorization checks.
Run middleBrick from your terminal to scan your Sails API:
npx middlebrick scan https://yourapp.com/api
# Or integrate into CI/CD
npx middlebrick scan --fail-below BThe scanner tests for BOLA (Broken Object Level Authorization) by systematically modifying resource IDs in authenticated requests and checking if access is properly restricted.
Check your Sails models for proper associations and filters. Waterline associations should include filters to prevent unauthorized data access:
// api/models/Post.js
module.exports = {
attributes: {
title: { type: 'string' },
content: { type: 'string' },
author: {
model: 'User',
required: true
}
},
beforeFind: async (criteria, proceed) => {
if (criteria.author === undefined) {
criteria.author = req.session.userId;
}
return proceed();
}
};Review your Sails configuration for CORS settings that might expose endpoints to unauthorized origins. In config/cors.js, restrict origins to only trusted domains:
module.exports.cors = {
origin: process.env.CORS_ORIGIN || 'https://yourapp.com',
credentials: true
};Sails-Specific Remediation
Remediating Cwe-284 in Sails requires a multi-layered approach. Start by implementing a robust authorization framework using Sails policies and custom middleware.
Create a comprehensive ownership check policy:
// api/policies/checkOwnership.js
module.exports = async (req, res, next) => {
const model = req.options.model;
const id = req.params.id;
if (!model || !id) return next();
const Model = sails.models[model];
if (!Model) return res.serverError('Unknown model');
let record;
try {
record = await Model.findOne({
id: id,
author: req.session.userId
});
} catch (err) {
return res.serverError(err);
}
if (!record) return res.forbidden('Access denied');
req.record = record;
return next();
};Apply this policy to all resource-modifying actions:
// config/policies.js
module.exports.policies = {
UserController: {
'find': [ 'isAuthenticated' ],
'update': [ 'isAuthenticated', 'checkOwnership' ],
'destroy': [ 'isAuthenticated', 'checkOwnership' ]
},
PostController: {
'create': [ 'isAuthenticated' ],
'update': [ 'isAuthenticated', 'checkOwnership' ],
'destroy': [ 'isAuthenticated', 'checkOwnership' ]
}
};For admin-level access, create a separate policy:
// api/policies/isAdmin.js
module.exports = async (req, res, next) => {
const user = await User.findOne(req.session.userId);
if (!user || !user.isAdmin) {
return res.forbidden('Admin access required');
}
return next();
};Modify your blueprint configuration to disable automatic REST routes and use custom actions instead:
// config/blueprints.js
module.exports.blueprints = {
actions: true,
rest: false,
shortcuts: false,
pluralize: false
};Implement controller actions with built-in authorization:
// api/controllers/PostController.js
module.exports = {
async find(req, res) {
const posts = await Post.find({
author: req.session.userId,
sort: 'createdAt DESC'
});
return res.json(posts);
},
async findOne(req, res) {
const post = await Post.findOne({
id: req.params.id,
author: req.session.userId
});
if (!post) return res.notFound();
return res.json(post);
},
async create(req, res) {
const postData = {
...req.body,
author: req.session.userId
};
const post = await Post.create(postData).fetch();
return res.status(201).json(post);
},
async update(req, res) {
const post = await Post.updateOne({
id: req.params.id,
author: req.session.userId
}, req.body);
if (!post) return res.notFound();
return res.json(post);
},
async destroy(req, res) {
const deleted = await Post.destroyOne({
id: req.params.id,
author: req.session.userId
});
if (!deleted) return res.notFound();
return res.json({ message: 'Post deleted' });
}
};Use Sails' lifecycle callbacks to enforce authorization at the model level:
// api/models/Post.js
module.exports = {
attributes: {
title: { type: 'string' },
content: { type: 'string' },
author: {
model: 'User',
required: true
}
},
beforeUpdate: async (values, proceed) => {
if (values.id) {
const post = await Post.findOne(values.id);
if (post.author !== req.session.userId) {
return proceed(new Error('Unauthorized'));
}
}
return proceed();
},
beforeDestroy: async (criteria, proceed) => {
const post = await Post.findOne(criteria);
if (!post || post.author !== req.session.userId) {
return proceed(new Error('Unauthorized'));
}
return proceed();
}
};Implement role-based access control using Sails configuration:
// config/rbac.js
module.exports.rbac = {
roles: {
user: {
can: ['read:own', 'update:own', 'delete:own']
},
admin: {
can: ['read:all', 'update:any', 'delete:any', 'create:any']
}
},
permissions: {
'read:own': (req, record) => record.author === req.session.userId,
'update:own': (req, record) => record.author === req.session.userId,
'delete:own': (req, record) => record.author === req.session.userId,
'read:all': () => true,
'update:any': () => true,
'delete:any': () => true,
'create:any': () => true
}
};Create a permission checking utility:
// api/hooks/rbac/index.js
module.exports = function (sails) {
return {
initialize: async (cb) => {
sails.permissionCheck = (action, record) => {
const user = req.session.user;
if (!user) return false;
const role = user.isAdmin ? 'admin' : 'user';
const rbac = sails.config.rbac;
const permission = rbac.permissions[action];
if (!permission) return false;
return permission(req, record);
};
return cb();
}
};
};