Broken Access Control in Feathersjs with Mongodb
Broken Access Control in Feathersjs with Mongodb — how this specific combination creates or exposes the vulnerability
Broken Access Control in a Feathersjs service backed by Mongodb often arises when authorization checks are absent or misapplied at the service handler level. Feathersjs is service-oriented and does not enforce authorization by default; if you define a service without adding custom hooks or method-level guards, any authenticated (or sometimes unauthenticated) user can invoke create, get, update, or remove operations. When combined with Mongodb as the backing store, the risks compound because query filters constructed from user input may inadvertently expose records if ownership or role constraints are not enforced.
Consider a typical Feathersjs service for user documents where the Mongodb collection stores a userId field to indicate ownership. If the service allows querying like { userId: { $in: userIds } } but does not validate that the requesting user is included in userIds, an attacker can manipulate parameters (e.g., omitting userId or injecting other values) to access or enumerate other users' data. This maps to OWASP API Top 10 A1:2023 Broken Object Level Authorization (BOLA)/Insecure Direct Object References (IDOR). Because Feathersjs can expose REST-like routes and GraphQL-like patterns, an attacker may probe endpoints using unauthenticated or low-privilege sessions to test whether valid resource identifiers return data they should not.
Moreover, Feathersjs hooks that modify queries must be carefully designed. A hook that appends a filter like { organizationId: orgId } based on a token claim can be bypassed if the hook does not consistently apply to all operations or if runtime parameters override the filter. For example, an update call with query.$set or an improperly constrained patch may allow altering fields that should be immutable for certain roles. Privilege escalation may occur when higher-privilege operations such as remove or field updates are permitted for roles that should only read. The use of Mongodb update operators like $set, $inc, or positional operators can inadvertently expose modification capabilities if the service does not validate which fields a subject is allowed to change.
Another vector involves property-level authorization. If a Feathersjs service returns full Mongodb documents containing sensitive fields such as passwordHash, role, or internal identifiers, and does not explicitly project or redact these fields based on user roles, information disclosure occurs. This is especially risky when the service relies on Mongodb's default retrieval behavior without a schema layer that enforces field visibility. Insecure direct object references can also manifest when users reference predictable _id values and the service returns them without verifying that the authenticated subject has the right to view or modify that specific document.
To detect these issues, scanning tools evaluate whether services implement consistent authorization at the endpoint level, validate ownership within query filters, and apply strict field-level permissions. They also check whether hooks uniformly enforce constraints across all CRUD methods and whether responses inadvertently expose sensitive attributes. Without these controls, the Feathersjs + Mongodb stack remains susceptible to unauthorized read, update, and delete actions that violate the principle of least privilege.
Mongodb-Specific Remediation in Feathersjs — concrete code fixes
Remediation centers on enforcing ownership and role checks within Feathersjs hooks and ensuring Mongodb queries are constrained by the subject's permissions. Below are concrete, working code examples that demonstrate secure patterns.
1. Enforce userId ownership in a before hook
Use a before hook to inject the authenticated user's ID into the query, preventing users from requesting other users' data.
const { authenticate } = require('@feathersjs/authentication').hooks;
app.service('documents').hooks({
before: {
all: [authenticate('jwt')],
find: [context => {
// context.params.user contains the decoded JWT payload
const { user } = context.params;
// Ensure every query for documents includes the userId filter
context.params.query = {
...context.params.query,
userId: user.id
};
return context;
}],
create: [context => {
const { user } = context.params;
// Assign ownership on creation
context.data.userId = user.id;
return context;
}],
update: [context => {
// Forbid updates that attempt to change userId
if (context.data.userId && context.data.userId !== context.params.user.id) {
throw new Error('Forbidden: Cannot change ownership');
}
// Ensure patch operations preserve original userId
context.params.query = { userId: context.params.user.id };
return context;
}],
patch: [context => {
// Constrain patch to the authenticated user's document
context.params.query = { userId: context.params.user.id };
return context;
}],
remove: [context => {
context.params.query = { userId: context.params.user.id };
return context;
}]
}
});
2. Apply role-based field filtering in after hooks
Use an after hook to remove sensitive fields from the returned Mongodb documents based on roles.
app.service('users').hooks({
after: {
all: [context => {
const { user } = context.params;
// If not an admin, remove passwordHash and role from each returned document
if (!user.roles.includes('admin')) {
context.result.data = context.result.data.map(doc => {
const { passwordHash, role, internalNotes, ...safeDoc } = doc;
return safeDoc;
});
}
return context;
}]
}
});
3. Validate and sanitize query parameters to prevent injection
Ensure incoming query filters cannot override security constraints by normalizing them before they reach Mongodb.
app.service('items').hooks({
before: {
find: [context => {
const baseQuery = { deleted: { $ne: true } };
// If the client provides a query, merge safely without allowing $where or $eval
const clientQuery = context.params.query || {};
// Remove potentially dangerous operators
const unsafeOps = ['$where', '$eval', '$script'];
unsafeOps.forEach(op => { delete clientQuery[op]; });
context.params.query = { $and: [baseQuery, clientQuery] };
return context;
}]
}
});
4. Use projection to limit returned fields
Explicitly define which fields are allowed for each operation to reduce exposure.
app.service('secrets').hooks({
before: {
find: [context => {
// Only return permitted fields for non-admin users
if (!context.params.user.roles.includes('admin')) {
context.params.query = context.params.query || {};
context.params.fields = { name: 1, email: 1, _id: 1 };
}
return context;
}]
}
});
5. Centralize authorization logic
For complex policies, implement a reusable authorization function that validates access against Mongodb metadata before proceeding.
const canAccessDocument = (user, document) => {
// Example: user must own the document or belong to the document's org
return document.userId === user.id || user.orgIds.includes(document.orgId);
};
app.service('records').hooks({
before: {
get: [async context => {
const record = await app.service('records').Model.findById(context.id);
if (!canAccessContext(params.user, record)) {
throw new Error('Forbidden');
}
context.params.record = record;
return context;
}]
}
});