Insecure Direct Object Reference in Feathersjs with Mongodb
Insecure Direct Object Reference in Feathersjs with Mongodb — how this specific combination creates or exposes the vulnerability
An Insecure Direct Object Reference (IDOR) occurs when an API exposes a reference to an internal object—such as a MongoDB document _id—and allows an authenticated user to access or modify that object without verifying that the user is authorized for that specific resource. In Feathersjs, services are often configured with minimal server-side ownership checks, relying on client-supplied IDs (e.g., :id in REST routes or the id field in a Feathers query) to locate and return data. When paired with MongoDB, this typically means using the _id provided by the client in a find, get, or patch call without confirming the requesting user’s relationship to that document.
Consider a Feathers service for user documents where the service definition does not scope queries by the authenticated user’s ID. If a client requests GET /documents/65abc1234567890abcdef123, Feathers passes the raw ID to MongoDB via an ObjectId lookup. Because the query does not include a user_id or team_id filter, the database returns the document if it exists, regardless of whether the requeter owns or is permitted to view it. This is especially risky when the ObjectId is predictable or when references are leaked through logs, error messages, or client-side state. Attackers can iterate through likely IDs or reuse IDs obtained from other sources (e.g., shared resources or previous sessions), leading to unauthorized read or modification of data.
Feathers hooks can inadvertently contribute to IDOR when they transform data or merge params without enforcing ownership. For example, a hook that adds populated fields or enriches the params object might expose additional references that the client can then manipulate. If a PATCH /documents/:id endpoint allows a user to change a status field but does not re-validate ownership in the hook or service method, the user might alter another user’s document by guessing or cycling through IDs. MongoDB’s flexible schema and support for nested documents can compound this when IDs are embedded within subdocuments or when references are used across collections without proper authorization checks.
Another common pattern is using URL-friendly slugs or custom identifiers that map to MongoDB _id values without a server-side access control layer. If the mapping is direct and the service does not validate that the authenticated user has rights to the mapped document, IDOR is effectively enabled. Additionally, if the Feathers app uses multi-tenancy (e.g., organization or tenant IDs), failing to include tenant filters in MongoDB queries means a user from one tenant can request data from another tenant by altering the tenant identifier in the request. This violates separation of duties and is a classic IDOR scenario amplified by the database’s permissive read path.
Real-world attack patterns mirror OWASP API Top 10 A1: Broken Object Level Authorization, and IDOR often intersects with authentication issues when tokens or session identifiers do not properly constrain access. For MongoDB, this can mean that a valid ObjectId, once discovered, remains accessible because the backend does not enforce user-level scoping at the query layer. CVE patterns that involve insecure direct object references in API frameworks typically highlight missing authorization checks on object retrieval and modification endpoints. With Feathersjs and MongoDB, the risk is elevated when developers assume the framework’s service layer implicitly enforces boundaries that it does not.
Mongodb-Specific Remediation in Feathersjs — concrete code fixes
To mitigate IDOR in Feathersjs with MongoDB, enforce user-level scoping in every query by injecting the authenticated user’s ID into the query filter and validating ownership before any database operation. Avoid relying on client-supplied IDs alone; instead, derive the effective user identifier from the authenticated payload (e.g., params.user._id) and construct MongoDB queries that combine the resource ID with the user context.
Below are concrete code examples for a Feathers service that manages documents, ensuring that each operation is scoped to the authenticated user.
Example: Safe GET with user scoping
// services/documents/documents.class.js
const { Service } = require('feathers-mongoose');
class DocumentsService extends Service {
async find(params) {
// Ensure user is authenticated
if (!params.user || !params.user._id) {
throw new Error('Unauthenticated');
}
// Scope the MongoDB query to the user's documents
params.query = params.query || {};
params.query.$and = [
{ _id: { $exists: true } }, // keep any existing _id filters if provided carefully
{ userId: params.user._id }
];
return super.find(params);
}
async get(id, params) {
if (!params.user || !params.user._id) {
throw new Error('Unauthenticated');
}
// Build a query that includes both the ID and user ownership
const doc = await this.Model.findOne({
_id: id,
userId: params.user._id
});
if (!doc) {
throw new Error('Document not found or access denied');
}
return doc;
}
}
module.exports = function (app) {
const options = {
Model: app.get('mongoose').model('Document'),
paginate: { default: 25, max: 50 }
};
app.use('/documents', new DocumentsService(options));
};
Example: Safe PATCH with hook-based ownership validation
// services/documents/hooks.js
const ensureOwnership = context => {
if (!context.params.user || !context.params.user._id) {
throw new Error('Unauthenticated');
}
// If an ID is provided in the query or payload, validate ownership
const id = context.id || (context.data && context.data._id);
if (!id) {
return Promise.resolve(context);
}
return context.app.service('documents').Model.findOne({
_id: id,
userId: context.params.user._id
}).then(doc => {
if (!doc) {
throw new Error('Document not found or access denied');
}
// Ensure the result is passed along for subsequent hooks
context.result = doc;
return context;
});
};
module.exports = {
before: {
patch: [ensureOwnership]
},
after: {
patch: []
},
error: {
patch: []
}
};
Example: Creating a document with user association
// In your service create method or a before hook
const createScopedDocument = context => {
if (!context.params.user || !context.params.user._id) {
throw new Error('Unauthenticated');
}
// Ensure the payload includes the userId from the authenticated user
context.data.userId = context.params.user._id;
// Optionally set createdBy for audit trails
context.data.createdBy = context.params.user._id;
return context;
};
// Then attach this hook to the create before chain
module.exports = {
before: {
create: [createScopedDocument]
}
};
In addition to scoping queries, use MongoDB projections to limit returned fields and avoid leaking sensitive data. Validate and sanitize any client-provided identifiers, even if they appear to be ObjectIds, to prevent NoSQL injection attempts that could alter query logic. Regularly audit service methods to ensure that every data access path includes user-level filters consistent with the authenticated subject.
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 |