Broken Access Control in Loopback
How Broken Access Control Manifests in Loopback
Broken Access Control in Loopback applications typically occurs through several Loopback-specific patterns. The most common is improper use of remotes.beforeRemote and remotes.afterRemote hooks where authorization checks are either missing or incorrectly implemented.
// Vulnerable pattern - missing authorization check
MyModel.beforeRemote('*', async (ctx, unused, next) => {
// No check if user is authenticated or has permissions
next();
});
Another frequent issue is the misuse of Loopback's accessType property. When developers set accessType: 'READ' on methods that should be restricted, any authenticated user can access sensitive data.
// Vulnerable - READ access allows anyone to view all user data
module.exports = function(User) {
User.remoteMethod('find', {
accessType: 'READ', // Should be 'WRITE' or require specific roles
returns: {arg: 'users', type: 'array'}
});
};
Loopback's dynamic scoping can also introduce BOLA (Broken Object Level Authorization) vulnerabilities. When using findById without proper ownership checks, users can access any record by ID.
// Vulnerable - no ownership verification
module.exports = function(Order) {
Order.findById = async function(id, options) {
return Order.findById(id); // Any user can access any order
};
};
The framework's default allowedRoles: $everyone setting on remote methods is another common pitfall. Developers often forget to restrict this, allowing unauthenticated access to sensitive operations.
Loopback-Specific Detection
Detecting Broken Access Control in Loopback requires examining both the codebase and runtime behavior. Start by reviewing all remotes.beforeRemote hooks for missing authorization logic.
# Search for vulnerable patterns
grep -r "beforeRemote" . | grep -v "authorize" | grep -v "role"
Check for exposed methods with overly permissive accessType settings. Any method that returns sensitive data should require specific roles or ownership verification.
// Scan for vulnerable accessType declarations
const vulnerableMethods = [];
for (const model of Object.values(app.models)) {
for (const method of Object.values(model.settings.remotes)) {
if (method.accessType === 'READ' && !method.allowedRoles.includes('admin')) {
vulnerableMethods.push(`${model.modelName}.${method.name}`);
}
}
}
middleBrick's black-box scanning approach is particularly effective for Loopback applications. It tests unauthenticated endpoints and verifies that protected routes actually require authentication.
# Scan Loopback API with middleBrick
middlebrick scan https://api.example.com/explorer
The scanner checks for common Loopback patterns like exposed /explorer endpoints, default CRUD operations without proper authorization, and methods that should require admin roles but don't.
For OpenAPI spec analysis, middleBrick resolves Loopback's $ref definitions and cross-references them with runtime findings to identify mismatches between documented security requirements and actual implementation.
Loopback-Specific Remediation
Fixing Broken Access Control in Loopback requires using the framework's built-in authorization features correctly. The most robust approach is implementing Loopback's AccessContext and AccessContextBuilder classes.
const {AccessContext, AccessContextBuilder} = require('@loopback/security');
module.exports = function(Order) {
Order.beforeRemote('findById', async (ctx, unused, next) => {
const userId = ctx.req.accessToken.userId;
const orderId = ctx.args.id;
// Verify ownership
const order = await Order.findById(orderId);
if (!order || order.customerId !== userId) {
const err = new Error('Access denied');
err.statusCode = 403;
return next(err);
}
next();
});
};
For role-based access control, use Loopback's AuthorizationBuilder to define granular permissions.
const {AuthorizationBuilder} = require('@loopback/authorization');
const authorization = new AuthorizationBuilder()
.allow(['admin', 'manager'])
.forResource('Order')
.forScope('read')
.build();
module.exports = function(Order) {
Order.beforeRemote('find', async (ctx, unused, next) => {
const authContext = new AccessContext({
principals: [ctx.req.principal],
resource: 'Order',
scopes: ['read']
});
if (!await authorization.authorize(authContext)) {
const err = new Error('Insufficient permissions');
err.statusCode = 403;
return next(err);
}
next();
});
};
Implement proper accessType restrictions on all remote methods:
module.exports = function(User) {
User.remoteMethod('find', {
accessType: 'READ',
allowedRoles: ['admin'], // Restrict to admin only
returns: {arg: 'users', type: 'array'}
});
};
For dynamic scoping vulnerabilities, always verify resource ownership before returning data:
module.exports = function(Document) {
Document.beforeRemote('find', async (ctx, unused, next) => {
const userId = ctx.req.accessToken.userId;
ctx.args.filter = ctx.args.filter || {};
ctx.args.filter.where = ctx.args.filter.where || {};
ctx.args.filter.where.ownerId = userId;
next();
});
};