Bola Idor in Sails
How BOLA/IdOR Manifests in Sails
Model-based authorization (BOLA) and Insecure Direct Object Reference (IdOR) vulnerabilities are particularly prevalent in Sails applications due to the framework's opinionated approach to data access and its common patterns for handling user authentication.
In Sails, the most common BOLA/IdOR pattern emerges through the req.user object and model queries. Consider a typical Sails controller action:
async findUserProfile(req, res) {
const user = await User.findOne({ id: req.params.id });
return res.json(user);
}The vulnerability here is subtle but critical: the code fetches a user by ID without verifying that the requesting user has permission to access that specific user's data. An attacker can simply increment the ID parameter to access other users' profiles.
Sails's blueprint API exacerbates this issue. When you enable blueprints without proper policies, Sails automatically generates RESTful endpoints that expose all model operations. A User model with blueprints enabled creates endpoints like:
GET /user/:id
GET /user/:id/profile
PUT /user/:id
PATCH /user/:idWithout explicit authorization checks, these endpoints become direct object reference vulnerabilities. An authenticated user can access any user record by knowing or guessing the ID.
Another Sails-specific manifestation occurs in association handling. Sails provides convenient methods for populating associations, but these can introduce BOLA vulnerabilities:
async getUserProjects(req, res) {
const projects = await Project.find({
where: { owner: req.params.id },
populate: ['tasks', 'team']
});
return res.json(projects);
}Here, the code fetches projects for any user ID provided in the URL without verifying the requester's relationship to those projects. An attacker can enumerate project IDs to access data they shouldn't see.
Sails Waterline ORM's query flexibility can also introduce BOLA vulnerabilities through improper filtering. Consider this common pattern:
async getUserMessages(req, res) {
const messages = await Message.find({
where: { recipient: req.params.id }
}).sort('createdAt DESC');
return res.json(messages);
}The vulnerability is that the code trusts the URL parameter for the recipient ID without validating that the authenticated user is actually the intended recipient.
Even more subtle BOLA issues arise in Sails policies. A policy might check if a user is authenticated but fail to verify resource ownership:
module.exports = async function(req, res, next) {
if (req.session.authenticated) {
return next();
}
return res.forbidden();
}This policy only checks authentication status, not whether the user should access the specific resource they're requesting.
Sails-Specific Detection
Detecting BOLA/IdOR vulnerabilities in Sails applications requires both static analysis of your codebase and dynamic runtime testing. Here's how to identify these issues specifically in Sails applications.
Static Code Analysis
Search your Sails controllers and services for patterns that indicate potential BOLA vulnerabilities. Look for:
grep -r "findOne(" controllers/ | grep -v "req.user.id"
grep -r "findById(" controllers/ | grep -v "req.user.id"
grep -r "find({" controllers/ | grep -v "where.*req.user"These patterns help identify queries that use URL parameters or other untrusted input for object identification without proper authorization checks.
Blueprint API Exposure
Check your config/blueprints.js for overly permissive settings:
module.exports.blueprints = {
actions: true,
rest: true,
shortcuts: true,
populate: true
};Blueprints with these settings expose CRUD operations without any authorization. Review each model to determine if blueprint access is appropriate.
Policy Implementation Audit
Examine your policies in api/policies/ to ensure they perform proper authorization, not just authentication:
module.exports = async function(req, res, next) {
const record = await Model.findOne({
id: req.params.id,
owner: req.user.id
});
if (!record) {
return res.forbidden();
}
return next();
}Policies should verify resource ownership, not just user authentication status.
Dynamic Testing with middleBrick
middleBrick's BOLA/IdOR scanner specifically tests Sails applications by:
- Analyzing your OpenAPI/Swagger spec (if available) to understand API structure
- Testing authenticated endpoints with parameter manipulation to detect unauthorized access
- Checking for common Sails patterns like
req.params.idusage without ownership verification - Testing blueprint endpoints for unauthorized data exposure
- Scanning for association-based vulnerabilities in populate operations
To scan your Sails API:
middlebrick scan https://yourapp.com/api
middlebrick scan --spec openapi.jsonThe scanner tests for BOLA vulnerabilities by attempting authenticated requests with manipulated parameters and checking if unauthorized data is returned.
Waterline Query Analysis
Review your Waterline queries for direct parameter usage:
const vulnerable = await Model.findOne({
id: req.params.id // vulnerable - no ownership check
});
const secure = await Model.findOne({
id: req.params.id,
owner: req.user.id // secure - verifies ownership
});The key indicator of BOLA vulnerability is using req.params.id, req.body.id, or similar untrusted input directly in queries without ownership validation.
Sails-Specific Remediation
Remediating BOLA/IdOR vulnerabilities in Sails requires a multi-layered approach using Sails's native features and security best practices. Here are specific remediation strategies for Sails applications.
Policy-Based Authorization
Create reusable policies for common authorization patterns:
// api/policies/ownerPolicy.js
module.exports = async function(req, res, next) {
const model = req.options.model;
const id = req.params.id;
const userId = req.user.id;
if (!model || !id || !userId) {
return res.forbidden();
}
try {
const record = await sails.models[model].findOne({
id: id,
owner: userId
});
if (!record) {
return res.forbidden();
}
req.record = record;
return next();
} catch (err) {
return res.serverError(err);
}
};Apply this policy to routes that require ownership verification:
// config/policies.js
module.exports.policies = {
UserController: {
findProfile: ['ownerPolicy', 'isAuthenticated'],
updateProfile: ['ownerPolicy', 'isAuthenticated']
},
ProjectController: {
find: ['ownerPolicy', 'isAuthenticated'],
update: ['ownerPolicy', 'isAuthenticated']
}
};Secure Controller Patterns
Refactor vulnerable controller actions to use secure patterns:
// Vulnerable pattern
async findUserProfile(req, res) {
const user = await User.findOne({ id: req.params.id });
return res.json(user);
}
// Secure pattern
async findUserProfile(req, res) {
const user = await User.findOne({
id: req.params.id,
owner: req.user.id
});
if (!user) {
return res.notFound();
}
return res.json(user);
}Blueprint Security Configuration
Configure blueprints with security in mind:
// config/blueprints.js
module.exports.blueprints = {
actions: true,
rest: true,
shortcuts: true,
populate: true,
// Add security middleware
beforeCreate: async (values, proceed) => {
if (values.owner === undefined) {
values.owner = req.user.id;
}
return proceed();
},
beforeUpdate: async (values, proceed) => {
if (values.owner !== undefined && values.owner !== req.user.id) {
return proceed(new Error('Cannot change record ownership'));
}
return proceed();
}
};Waterline Query Security
Use Waterline's query capabilities to enforce authorization at the database level:
// Secure find with multiple conditions
async getUserData(req, res) {
const userId = req.user.id;
const targetId = req.params.id;
// Only allow access to own data or admin access
const isAdmin = await User.isAdmin(userId);
const query = { id: targetId };
if (!isAdmin) {
query.owner = userId;
}
const user = await User.findOne(query);
if (!user) {
return res.notFound();
}
return res.json(user);
}Association Security
Secure association queries in Sails:
// Vulnerable
async getUserProjects(req, res) {
const projects = await Project.find({
where: { owner: req.params.id },
populate: ['tasks', 'team']
});
return res.json(projects);
}
// Secure
async getUserProjects(req, res) {
const userId = req.user.id;
const targetId = req.params.id;
// Verify requester has access to target user
const canAccess = await canUserAccessTarget(userId, targetId);
if (!canAccess) {
return res.forbidden();
}
const projects = await Project.find({
where: { owner: targetId },
populate: ['tasks', 'team']
});
return res.json(projects);
}Input Validation and Sanitization
Always validate and sanitize ID parameters:
async secureFind(req, res) {
const id = req.params.id;
if (!isValidObjectId(id)) {
return res.badRequest('Invalid ID format');
}
const record = await Model.findOne({
id: id,
owner: req.user.id
});
if (!record) {
return res.notFound();
}
return res.json(record);
}Testing with middleBrick
After implementing these remediations, verify your fixes using middleBrick's continuous scanning:
middlebrick scan --continuous --fail-threshold BThis ensures your BOLA vulnerabilities remain fixed as your codebase evolves.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |