Insecure Direct Object Reference in Feathersjs with Jwt Tokens
Insecure Direct Object Reference in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Insecure Direct Object Reference (IDOR) occurs when an API exposes a reference — such as a database key or an identifier — in a request and relies solely on the caller’s identity to enforce access control. In FeathersJS, this commonly manifests when a service route uses a user-supplied ID (e.g., userId, postId) to look up a record without verifying that the requesting subject is authorized to access that specific object. When JWT tokens are used for authentication, the token typically carries a subject (sub) and possibly roles or scopes, but if the application uses the token only to identify the user and then directly uses user-provided IDs to query data, the authorization boundary is incomplete.
Consider a FeathersJS service defined with feathers-sequelize or feathers-mongoose. A route like /users/:id may retrieve a user record by taking the :id parameter and querying the database. If the route handler does not ensure that the authenticated user’s subject matches the requested :id (or that the user has an admin role), an attacker can manipulate the ID in the request to access other users’ data. JWT tokens provide integrity and identity, but they do not automatically enforce object-level permissions. For example, an attacker with a valid token for a regular user could change /users/100 to /users/101 and, without proper authorization checks, gain access to another user’s record. This becomes especially relevant when IDs are predictable (sequential integers or UUIDs) and when the service does not scope queries by the authenticated subject.
In a FeathersJS application, the vulnerability is not caused by JWT handling itself, but by the combination of JWT-based authentication and missing or insufficient authorization logic at the service layer. Middleware may set params.user from the decoded token, but if a hook or service method uses app.service('todos').get(params.id) without confirming that params.id belongs to the user in params.user, the boundary is broken. Real-world attack patterns include changing numeric IDs, tampering with references in nested resources (e.g., /organizations/123/members/456), or exploiting weak indirect object references in query parameters. Compounded with overly permissive service permissions or misconfigured hooks, IDOR allows horizontal privilege escalation where one user can act on another’s resources.
Because FeathersJS encourages a service-oriented architecture with hooks for cross-cutting concerns, developers must explicitly enforce ownership or role-based checks within hooks or custom methods. Relying on the framework’s default behavior or assuming that JWT authentication implies full authorization is a common pitfall. For instance, a before hook that only verifies the token’s validity but does not scope the query to the authenticated user leaves the endpoint vulnerable. Attackers can probe such endpoints with modified IDs to enumerate accessible resources, leading to data exposure as outlined in the OWASP API Top 10. Therefore, securing the combination of JWT tokens and resource references in FeathersJS requires deliberate authorization logic that ties each request to the authenticated subject.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
To remediate IDOR in FeathersJS when using JWT tokens, enforce that every data access is scoped to the authenticated subject derived from the token. Store the user identifier in the JWT (commonly as the sub claim) and ensure that service methods compare this identifier with the requested resource identifier before proceeding.
Example: a users service that safely resolves the requestor’s own profile.
// src/services/users/users.class.js
const { Service } = require('feathers-sequelize');
class UsersService extends Service {
async get(id, params) {
const { user } = params;
// Ensure the authenticated user can only retrieve their own record unless elevated
if (id !== user.sub) {
throw new Error('Not found');
}
return super.get(id, params);
}
}
module.exports = function () {
const app = this;
app.use('/users', new UsersService({ Model: app.get('knex'), paginate: { default: 10 } }));
const usersService = app.service('users');
usersService.hooks({
before: {
all: [async context => {
const { user } = context.params;
if (!user) {
throw new Error('Unauthenticated');
}
// Attach user sub for use in the service method
context.params.ownerId = user.sub;
}]
},
after: {
all: [],
error: [],
success: []
}
});
};
The hook ensures that params.ownerId is available in service methods. The service then uses it to scope queries.
Example: a todos service where users can only access their own todos, even when an ID is supplied.
// src/services/todos/todos.class.js
const { Service } = require('feathers-sequelize');
class TodosService extends Service {
async find(params) {
const { user, ownerId } = params;
// Scope query to the authenticated user’s todos
params.sequelize = {
where: {
userId: ownerId
}
};
return super.find(params);
}
async get(id, params) {
const { ownerId } = params;
const record = await super.get(id, params);
if (record.userId !== ownerId) {
throw new Error('Not found');
}
return record;
}
}
module.exports = function () {
const app = this;
app.use('/todos', new TodosService({ Model: app.get('knex'), paginate: { default: 10 } }));
app.service('todos').hooks({
before: {
all: [async context => {
const { user } = context.params;
if (user) {
context.params.ownerId = user.sub;
}
}]
}
});
};
These patterns ensure that the ID supplied by the client is not used in isolation; instead, the query is constrained by the authenticated subject. For list endpoints, scoping via params.sequelize.where (or equivalent for Mongoose) prevents enumeration of other users’ records. For single-record endpoints, re-validate ownership after retrieval or embed the ownership check in the base query to avoid leaking existence via timing differences.
Additionally, limit the claims in the JWT to what is necessary and avoid including sensitive data in the payload. Use short-lived tokens and refresh strategies to reduce the impact of token leakage. Combine these practices with role-based access control checks in hooks when admin operations are required, ensuring that elevated permissions are granted explicitly and verified server-side.
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 |