HIGH insecure direct object referencefeathersjsjwt tokens

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 IDNameSeverity
CWE-250Execution with Unnecessary Privileges HIGH
CWE-639Insecure Direct Object Reference CRITICAL
CWE-732Incorrect Permission Assignment HIGH

Frequently Asked Questions

Can IDOR occur even when JWT tokens are properly validated?
Yes. JWT validation confirms identity and integrity of the token, but it does not enforce object-level permissions. If the application uses user-provided IDs to access resources without verifying that the authenticated subject is allowed to access that specific ID, IDOR can still occur.
How can I test for IDOR in my FeathersJS API?
Use an authenticated request with one user’s token and modify resource IDs (e.g., change :id or path parameters) to see whether records belonging to other users are returned. Automated scanners like middleBrick can surface IDOR findings by correlating authentication context with object references; always complement automated tests with targeted manual checks.