Broken Access Control in Sails (Javascript)
Broken Access Control in Sails with Javascript — how this specific combination creates or exposes the vulnerability
Broken Access Control is an OWASP API Top 10 category that commonly manifests in Sails.js applications written in JavaScript when authorization checks are missing, incomplete, or bypassed at the controller or policy layer. Sails provides a policy system that can run synchronously or asynchronously, but if policies are not applied consistently to every route that touches sensitive data or administrative actions, an unauthenticated or low-privilege user can reach endpoints that should be restricted.
In a typical Sails app, controllers expose actions that map to routes, and policies are attached either globally, per controller, or per action. If a developer forgets to add an authorization policy to an action, or relies only on frontend UI hiding (security through obscurity), an attacker can call the endpoint directly. For example, a JavaScript controller action that returns user records without checking whether the requesting user has permission to view those records enables IDOR/Insecure Direct Object References. Sails Waterline ORM does not automatically enforce row-level permissions; it is the developer’s responsibility to scope queries to the requesting user or tenant.
Additionally, Sails blueprints can inadvertently expose create, read, update, and delete (CRUD) routes if blueprint actions are enabled globally in config/controllers.js. When combined with weak or missing ownership checks in the model or policy, this allows horizontal privilege escalation where one user can operate on another user’s resources. Vertical privilege escalation can occur if an attacker adds an administrative flag to their payload or reaches admin-only routes because the authorization policy is not enforced for certain HTTP verbs or paths.
Because Sails applications often expose REST-like endpoints via controllers and policies, a missing or incorrect check in a controller method or policy can lead to sensitive data exposure, unauthorized modification, or deletion. For instance, a policy that only checks for authentication (via req.isAuthenticated()) but not for authorization (role or scope validation) satisfies the check but still leaks or mutates data. In JavaScript, this often means using truthy role strings or numeric IDs without verifying hierarchy, tenant, or consent, which can be manipulated if input validation is weak.
When using OpenAPI/Swagger specs with Sails, it is important to ensure that spec-defined security schemes are actually enforced by the runtime controllers and policies. MiddleBrick’s scans compare the declared spec security requirements against runtime behavior; if an endpoint is marked as requiring a scope or role but the Sails controller does not validate it, the scan will flag this as a Broken Access Control finding with remediation guidance to add explicit authorization checks.
Javascript-Specific Remediation in Sails — concrete code fixes
Remediation in Sails with JavaScript focuses on explicit authorization in controllers or policies, scoping database queries, and ensuring blueprint routes are not unintentionally exposed. Below are concrete, working examples.
1. Apply a policy and enforce ownership in the controller
Define a policy that checks that the requesting user owns the resource or has the required role. Then attach the policy to the controller action and scope the Waterline query to the user.
// api/policies/ensureOwnership.js
module.exports = async function ensureOwnership(req, res, next) {
const userId = req.user.id; // authenticated user ID from session or token
const recordId = req.param('id');
if (!userId) {
return res.unauthorized('Authentication required');
}
// Fetch the record and verify ownership
const record = await User.findOne(recordId);
if (!record) {
return res.notFound();
}
if (record.id !== userId) {
return res.forbidden('You do not have permission to access this resource');
}
// Attach the record to req for downstream use
req.record = record;
return next();
};
// config/policies.js
module.exports.policies = {
UserController: {
profile: ['ensureOwnership'],
update: ['ensureOwnership']
}
};
// api/controllers/UserController.js
module.exports = {
profile: async function (req, res) {
// req.record is guaranteed to belong to req.user.id
return res.ok(req.record);
},
update: async function (req, res) {
const updated = await User.updateOne(req.param('id')).set(req.body);
if (!updated) {
return res.notFound();
}
return res.ok(updated);
}
};
2. Scope blueprint routes and disable public access where unnecessary
If you use Sails blueprints, restrict them by disabling actions or applying policies. For sensitive models, disable blueprint actions and implement explicit controller endpoints with proper authorization.
// config/controllers.js
module.exports.controllers = {
// Disable blueprint actions for the Account model
blueprints: {
actions: false,
rest: false
}
};
// api/controllers/AccountController.js
module.exports = {
list: async function (req, res) {
// Only allow admins to list accounts
if (!req.user || !['admin'].includes(req.user.role)) {
return res.forbidden('Insufficient permissions');
}
const accounts = await Account.find();
return res.ok(accounts);
},
create: async function (req, res) {
// Enforce input validation and ownership/role checks
if (!req.isAuthenticated) {
return res.unauthorized();
}
// Example: regular users can only create their own accounts
if (req.user.role !== 'admin') {
req.body.userId = req.user.id;
}
const account = await Account.create(req.body).fetch();
return res.created(account);
}
};
3. Validate input and avoid insecure direct object references
Always validate parameters and avoid exposing internal IDs directly. Use permalinks or indirect references where appropriate, and enforce scope checks in policies.
// api/policies/requireScope.js
module.exports = async function requireScope(req, res, next) {
const allowedScopes = req.user.scopes || [];
const required = req.options.requiredScopes;
if (!required) return next();
const hasAll = required.every(r => allowedScopes.includes(r));
if (!hasAll) {
return res.forbidden('Missing required scope');
}
return next();
};
// Example usage in a policy assignment
module.exports.policies = {
PaymentController: {
refund: ['requireScope'],
view: ['requireScope']
}
};
// In a route config or controller, define requiredScopes
// This can be read by MiddleBrick to compare spec expectations vs actual enforcement
By combining explicit policies, scoped queries, and careful blueprint configuration, you mitigate Broken Access Control in Sails JavaScript applications. Regular scans with tools like MiddleBrick help ensure declared security requirements are reflected in runtime behavior.