Insecure Direct Object Reference in Feathersjs with Basic Auth
Insecure Direct Object Reference in Feathersjs with Basic Auth — how this specific combination creates or exposes the vulnerability
Insecure Direct Object Reference (IDOR) occurs when an API exposes internal object identifiers (e.g., database IDs, filenames) without verifying that the authenticated subject has permission to access the specific resource. In FeathersJS, which is often used with REST transports and can rely on Basic Auth, IDOR arises when endpoints accept user-supplied IDs and directly fetch or operate on records without an authorization check scoped to the requesting identity.
When Basic Auth is used, the client sends an Authorization: Basic base64(credentials) header. FeathersJS can validate credentials via an authenticate hook and attach a user object (e.g., with an id) to the request context (params.user). However, if a service route like /users/:id or /accounts/:accountId/transactions/:transactionId uses that parameter directly to query the service (for example, via app.service('accounts').get(accountId)) without confirming that params.user.id === accountId (or equivalent ownership), the endpoint becomes vulnerable to IDOR.
Consider a Feathers service for user profiles. An attacker authenticated with Basic Auth as a low-privilege user might iterate over numeric IDs in GET /users/123. If the server does not enforce that the authenticated user can only access their own profile, the attacker can read other users’ data. This is an IDOR at the business logic level, not a missing authentication issue: authentication succeeded, but authorization was missing or insufficient. In Feathers, common root causes include missing or misconfigured hooks, overly broad find filters that do not scope to params.user, or custom get/remove methods that use IDs without ownership checks.
OpenAPI/Swagger analysis can surface some IDOR risks by revealing endpoints with user-controlled path parameters. However, runtime behavior depends on hook implementation and data access patterns. For example, an endpoint defined as GET /accounts/:id in the spec may appear safe, but if the handler ignores params.user and returns the account for :id regardless of caller, the spec does not capture the authorization gap. middleBrick’s checks for BOLA/IDOR and Property Authorization are designed to detect such runtime mismatches, especially when combined with unauthenticated or Basic Auth-tested attack surfaces.
In Feathers, an additional nuance is that services can be nested or use custom methods (e.g., getMyTransactions) that still accept an ID. If these methods do not validate that the ID belongs to the requesting user, they introduce IDOR even when the default find or get is properly scoped. Transport-specific concerns (e.g., REST vs Socket) can also affect how hooks receive params, making consistent authorization checks across all transports essential.
To illustrate, here is an insecure Feathers service setup where IDOR is possible despite Basic Auth being used for authentication:
// Insecure example: missing ownership check in a Feathers service hook
const { AuthenticationError } = require('@feathersjs/errors');
// Assume this hook only ensures a user is authenticated, not that they own the resource
const ensureAuthenticated = context => {
if (!context.params.user) {
throw new AuthenticationError('Authentication required');
}
return context;
};
// A route like GET /messages/:id will allow user A to read user B’s message if :id is provided directly
app.use('/messages', {
find: async params => {
// Missing filter to scope to params.user.id
return await app.service('messages').Model.find(params.query);
},
get: async id => {
// IDOR: does not verify that the message belongs to params.user
return await app.service('messages').Model.findById(id);
}
});
app.configure(hooks => {
hooks.find.push(ensureAuthenticated);
hooks.get.push(ensureAuthenticated);
});
In this example, authentication is enforced, but the service does not ensure that a message with a given ID belongs to the authenticated user. An attacker with valid Basic Auth credentials can enumerate IDs and access others’ messages, demonstrating IDOR in the context of FeathersJS with Basic Auth.
Basic Auth-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on scoping data access to the authenticated subject. In FeathersJS, this means using params.user (populated after authentication) in hooks to enforce that operations only affect resources owned by or authorized for the user. Below are concrete, syntactically correct examples that show how to implement these checks.
1. Scoped find with ownership filter:
// Secure find: restrict results to the authenticated user’s records
const { iff, isProvider } = require('feathers-hooks-common');
const restrictToSelf = context => {
if (context.params.user) {
// For REST transport, query.params may be used; for socket, same pattern applies
context.params.query = context.params.query || {};
context.params.query.userId = context.params.user.id;
}
return context;
};
app.use('/messages', {
before: {
find: [iff(isProvider('external'), restrictToSelf)]
}
});
This ensures that a GET /messages request only returns messages where userId matches the authenticated user’s ID, preventing IDOR in listing operations.
2. Secure get with ownership verification:
// Secure get: verify ownership before returning a single record
const { iff, isProvider } = require('feathers-hooks-common');
const { GeneralError } = require('@feathersjs/errors');
const ensureOwnership = async context => {
const { user } = context.params;
if (!user) return context;
const record = await context.app.service(context.path).Model.findById(context.id);
if (!record) {
throw new GeneralError('Not found', { code: 404 });
}
if (String(record.userId) !== String(user.id)) {
throw new GeneralError('Forbidden', { code: 403 });
}
return context;
};
app.use('/messages', {
before: {
get: [iff(isProvider('external'), ensureOwnership)]
}
});
This hook fetches the record and compares its userId with the authenticated user’s ID, raising a 403 if they differ. This directly mitiplies IDOR for item-level endpoints.
3. Example with Basic Auth authentication hook (using @feathersjs/authentication-local):
const authentication = require('@feathersjs/authentication');
const local = require('@feathersjs/authentication-local');
app.configure(authentication({
entity: 'user',
service: 'users',
setup: app => {
app.use('/users', {
// Example: ensure that user can only access their own profile
before: {
get: [async context => {
const { user } = context.params;
if (user && context.id === user.id) {
return context;
}
throw new Forbidden();
}]
}
});
}
}));
app.configure(local());
In this setup, authentication hooks populate context.params.user. The get hook then enforces that the requested user ID matches the authenticated user’s ID, closing the IDOR vector for profile endpoints.
4. Transport-specific considerations: For Socket.io transports, the same hooks apply, but ensure that the ID is not taken from untrusted query parameters without validation. Always derive the user identity from context.params.user rather than from the payload or URL alone.
These remediation patterns ensure that even when Basic Auth is used, each operation enforces ownership or role-based checks, effectively mitigating IDOR in FeathersJS services.
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 |