Broken Access Control in Loopback with Mongodb
Broken Access Control in Loopback with Mongodb — how this specific combination creates or exposes the vulnerability
Broken Access Control in a Loopback application that uses Mongodb as a data store often arises when authorization checks are performed at the Mongodb query layer instead of at the application resource layer. Loopback models expose data sources and relations, and if a developer relies solely on model-level or Mongodb query filters without validating the authenticated user’s rights to a specific instance, an authenticated user can manipulate request parameters to access or modify another user’s records.
Consider a typical setup where a Loopback model is backed by a Mongodb collection, such as a Project model mapped to a projects collection. If the model’s scope or a Mongodb query filter is defined with a static or poorly parameterized condition (for example, only filtering by teamId without verifying the requesting user’s membership), an attacker can change the teamId in the request to gain access to projects belonging to other teams. This becomes a Broken Access Control issue because the authorization boundary is weak: authentication may succeed, but authorization is either missing or incorrectly scoped to the Mongodb query rather than to the business entity and user context.
A concrete example is a controller method that constructs a Mongodb query using user-supplied input without ensuring the user’s role or ownership. For instance, an endpoint like GET /projects?teamId=xyz might build a Mongodb filter { teamId: req.query.teamId } without confirming that the authenticated user belongs to that team in the application’s authorization model. Because Loopback can automatically expose model endpoints, an attacker can supply a different teamId to enumerate or manipulate data across teams. Even with Mongodb-level filtering, if the application does not enforce user-to-resource mappings (such as checking a user’s teams via a separate membership collection), the control is effectively bypassed.
Another common pattern is the use of Model hooks or scoped filters that are not applied consistently across all data access paths. For example, a scope defined in the Loopback model JSON might filter by { isArchived: { ne: true } }, but if a custom remote method builds its own Mongodb query without including the same scoping condition, an attacker could access archived resources. Additionally, if the Mongodb documents embed sensitive fields (e.g., roles, permissions) and the Loopback model exposes them via relations or through the REST API without proper property filtering, information exposure can occur alongside Broken Access Control.
These issues map to the OWASP API Top 10 category A01:BROKEN ACCESS CONTROL. Unlike infrastructure-level controls, application-level authorization must be explicit for each resource request. middleBrick’s checks for BOLA/IDOR and Property Authorization are designed to detect cases where object identifiers or properties are exposed without proper ownership or role checks, including in ODM layers like Mongodb-backed Loopback models.
Mongodb-Specific Remediation in Loopback — concrete code fixes
To remediate Broken Access Control when using Mongodb in Loopback, enforce user-centric authorization in application logic before constructing any Mongodb query. Always resolve the requesting user’s identity and permissions, then build query filters that incorporate user context, such as team membership or ownership fields. Avoid exposing internal IDs directly and prefer application-level identifiers that the backend can validate against the user’s permissions.
Below are concrete code examples for a Loopback model named Project backed by Mongodb, illustrating proper authorization checks.
1. Remote method with explicit user-to-project ownership check
module.exports = function(Project) {
Project.getForUser = function(userId, projectId, cb) {
// First, confirm the user exists and fetch their teams (or ownership list)
const User = require('./user.model');
User.findById(userId, { include: 'teams' }, function(err, user) {
if (err || !user) return cb(err);
// Ensure the projectId belongs to one of the user’s teams
const teamIds = user.teams.map(t => t.id);
const mongodb = Project.dataSource.connector.db;
const collection = mongodb.collection('projects');
collection.findOne({
_id: projectId,
teamId: { $in: teamIds }
}, function(err, project) {
if (err) return cb(err);
cb(null, project || { error: 'Not found' });
});
});
};
Project.remoteMethod('getForUser', {
http: { path: '/:projectId', verb: 'get' },
accepts: [
{ arg: 'userId', type: 'string', required: true, http: { source: 'query' } },
{ arg: 'projectId', type: 'string', required: true, http: { source: 'params' } }
],
returns: { arg: 'project', type: 'object' },
description: 'Get a project only if the user belongs to its team.'
});
};
2. Scoped model access via Loopback relations with proper filters
If you model team membership as a relation, ensure that queries through relations also enforce scope. For example, define a Team model that has many Projects, and configure the relation with a scope that filters by team membership. Then, when accessing projects via the team relation, Loopback applies the filter automatically.
// In team.model.json
{
"name": "Team",
"relations": {
"projects": {
"type": "hasMany",
"model": "project",
"foreignKey": "teamId"
}
}
}
// In project.model.json
{
"name": "Project",
"options": {
"scope": {
"where": {}
}
}
}
// Controller usage: team.projects() will only return projects where teamId matches
// Ensure the remote method validates team access before exposing the relation
module.exports = function(Team) {
Team.listProjects = function(teamId, userId, cb) {
const TeamModel = require('./team.model');
TeamModel.findById(teamId, function(err, team) {
if (err || !team) return cb(err);
// Verify user is a member before returning projects
// (membership check should be implemented separately)
team.projects(function(err, projects) {
cb(err, projects || []);
});
});
};
Team.remoteMethod('listProjects', {
accepts: [
{ arg: 'teamId', type: 'string', required: true },
{ arg: 'userId', type: 'string', required: true }
],
returns: { arg: 'projects', type: ['project'] },
description: 'List projects for a team after validating user membership.'
});
};
3. Avoid exposing internal IDs; use slugs or team-scoped identifiers
Where possible, avoid using raw ObjectId values in URLs. Instead, use a composite or team-scoped identifier that can be validated against the user’s accessible set. This reduces the risk of ID manipulation attacks that lead to Broken Access Control.
module.exports = function(Project) {
Project.findByTeamRef = function(teamRef, projectRef, userId, cb) {
const mongodb = Project.dataSource.connector.db;
const collection = mongodb.collection('projects');
collection.findOne({
teamRef: teamRef,
projectRef: projectRef,
// Ensure the user has access to this team
teamMembers: userId
}, function(err, project) {
cb(err, project);
});
};
Project.remoteMethod('findByTeamRef', {
http: { path: '/by-team-ref', verb: 'get' },
accepts: [
{ arg: 'teamRef', type: 'string', required: true },
{ arg: 'projectRef', type: 'string', required: true },
{ arg: 'userId', type: 'string', required: true }
],
returns: { arg: 'project', type: 'object' },
description: 'Find project by team and project reference with user access check.'
});
};
These patterns emphasize that authorization must be enforced in application code and reflected in Mongodb queries. Even when using ODM features, always validate that the requesting user has the necessary relationship to the resource before constructing filters. middleBrick can help identify gaps where such checks are missing through its Property Authorization and BOLA/IDOR assessments.