Bola Idor in Sails with Bearer Tokens
Bola Idor in Sails with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA) occurs when an API lacks proper authorization checks such that one user can view or modify another user’s resources. In Sails.js, this commonly arises when controllers look up records by identifier (e.g., a numeric ID or UUID) and return them without verifying that the requesting user is entitled to access that object. When Bearer Tokens are used for authentication, the risk increases if token validation is decoupled from object-level authorization, or if the token’s associated user identity is not consistently enforced across endpoints.
Consider a Sails API that uses JWT Bearer Tokens where the payload includes a user ID (sub). An endpoint like GET /api/users/:id should ensure that the requesting user’s ID from the token matches the :param.id. If the controller simply does User.findOne(req.param('id')), an attacker who knows or guesses another user’s ID can read or act on that resource despite presenting a valid Bearer Token. The token proves identity but does not prove authorization for that specific object. Sails Waterline ORM does not automatically enforce ownership; developers must explicitly add checks. Missing checks are more likely when APIs are built quickly or when developers assume route parameters are trustworthy. This is a BOLA flaw: authorization is bypassed by manipulating the object identifier while authentication (Bearer Token) remains valid.
Additionally, indirect object references can expose BOLA in Sails when using associations. For example, a request to GET /api/accounts/:accountId/transactions/:txId might validate the Bearer Token and confirm the transaction belongs to the account, but if the developer does not also confirm the account belongs to the requesting user, an attacker can iterate over account IDs to access another user’s transactions. Inconsistent authorization across nested resources amplifies BOLA. The API might return 200 with sensitive data or allow DELETE/PUT/PATCH on objects the user should not touch. This violates the principle of least privilege and can lead to mass data exposure or tampering.
Bearer Tokens themselves do not cause BOLA, but they can obscure the issue if token introspection or decoding is relied upon for identity without reinforcing object-level checks. In Sails, if policies or middleware only verify the token’s signature and expiry, and controllers trust user-supplied IDs, the authorization boundary is weak. Attackers do not need to compromise the token; they simply manipulate identifiers. The combination of a flexible framework like Sails, common use of Bearer Tokens for APIs, and missing per-request authorization creates a practical path for BOLA. Proper remediation requires validating that the resource’s owner matches the token’s subject on every data access, using model policies or service-layer checks, and ensuring referential integrity across associations.
Bearer Tokens-Specific Remediation in Sails — concrete code fixes
Remediation centers on ensuring that for every object access, the controller confirms the requesting user (from the Bearer Token) owns or is authorized to interact with that object. Below are concrete, realistic Sails code examples that demonstrate secure patterns.
Example 1: Securing a user profile endpoint
Assume an endpoint GET /api/users/me that returns the current user’s profile using a Bearer Token containing sub. The token is verified by an auth middleware that attaches req.user (with id and sub). The controller should use req.user.id and avoid trusting req.param('id').
// api/controllers/UserController.js
async me(req, res) {
// req.user is set by auth middleware from Bearer Token payload
if (!req.user) {
return res.unauthorized({ error: 'Unauthorized' });
}
// Do not use req.param('id'); use the identity from the token
const userId = req.user.id;
const user = await User.findOne(userId);
if (!user) {
return res.notFound({ error: 'User not found' });
}
return res.ok(user);
}
This ensures the ID used is derived from the token, not from the request, preventing ID swapping attacks.
Example 2: Securing a nested resource with ownership check
For endpoints like GET /api/accounts/:accountId/transactions/:txId, verify both the account belongs to the user and the transaction belongs to that account.
// api/controllers/TransactionController.js
async show(req, res) {
const { accountId, txId } = req.params;
// Verify Bearer Token user exists
if (!req.user || !req.user.id) {
return res.unauthorized({ error: 'Unauthorized' });
}
// Confirm the account belongs to the requesting user
const account = await Account.findOne({
id: accountId,
userId: req.user.id
});
if (!account) {
return res.forbidden({ error: 'Access denied to this account' });
}
// Confirm the transaction belongs to the verified account
const transaction = await Transaction.findOne({
id: txId,
accountId: account.id
});
if (!transaction) {
return res.notFound({ error: 'Transaction not found' });
}
return res.ok(transaction);
}
This double-check pattern prevents BOLA across associations by ensuring the requesting user owns the parent and the child record is linked to that parent.
Example 3: Policy-based enforcement using Sails policies
Define a policy that checks object ownership for sensitive routes. Policies run before controllers and can short-circuit unauthorized requests.
// api/policies/ensureOwnership.js
module.exports = async function ensureOwnership(req, res, next) {
// Expecting req.user from auth and a model name and record id in route options
const { model, id } = req.options;
if (!req.user || !model || !id) {
return res.forbidden({ error: 'Forbidden' });
}
const record = await model.findOne(id);
if (!record) {
return res.notFound({ error: 'Not found' });
}
// Assume records have a userId attribute linking to the token subject
if (record.userId !== req.user.id) {
return res.forbidden({ error: 'Access denied to this resource' });
}
return next();
};
Apply the policy in config/policies.js for routes that require ownership checks. This centralizes authorization logic and reduces the chance of missing checks in individual controllers.
Example 4: Creating resources with correct ownership
When creating child records, explicitly set the user reference from the token rather than trusting client input.
// api/controllers/TransactionController.js
async create(req, res) {
if (!req.user) {
return res.unauthorized({ error: 'Unauthorized' });
}
const { accountId, amount, currency } = req.body;
// Ensure the provided accountId belongs to the user
const account = await Account.findOne({
id: accountId,
userId: req.user.id
});
if (!account) {
return res.forbidden({ error: 'Invalid account or access denied' });
}
const transaction = await Transaction.create({
accountId: account.id,
amount,
currency,
userId: req.user.id // explicitly set ownership
}).fetch();
return res.created(transaction);
}
By tying object ownership to the Bearer Token subject and validating on read, write, and associate operations, BOLA is effectively mitigated in Sails applications using Bearer Tokens.
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 |