HIGH sandbox escapefeathersjsjwt tokens

Sandbox Escape in Feathersjs with Jwt Tokens

Sandbox Escape in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability

FeathersJS is a framework for real-time applications that commonly uses JWT tokens for stateless authentication. A sandbox escape in this context refers to an authenticated context—established via JWT—being used to access or manipulate data or services that should be restricted by scope, role, or tenant boundaries. When FeathersJS services rely solely on the presence of a valid JWT without enforcing strict authorization checks on each service method, an authenticated request may be able to reach functionality or data that should remain isolated.

Consider a FeathersJS application where authentication is configured via JWT and a typical service file exposes CRUD operations without additional record-level ownership or tenant checks. If the service does not scope queries using the authenticated subject (e.g., user ID from the JWT payload) or enforce that a user can only operate on their own resources, an authenticated user may inadvertently or intentionally access another user’s data. This is not a flaw in JWT itself, but rather a failure to enforce authorization relative to the identity carried in the token. The presence of a valid JWT establishes identity but does not automatically enforce permissions, which can lead to Insecure Direct Object References (IDOR)—a common precursor to sandbox escape in multi-tenant or multi-user systems.

In FeathersJS, this can occur when hooks or service methods assume that because a JWT is valid, the caller is authorized for any resource matching a simple identifier. For example, a service route like /users/ may allow GET if the JWT is valid, without verifying that the authenticated user’s ID matches the requested . An attacker with a valid JWT for their own account could change the identifier in the request and access other users’ profiles or administrative endpoints if the server does not enforce strict ownership checks or role-based access controls. This becomes a sandbox escape when the attacker leverages their authenticated JWT to traverse beyond their intended operational boundaries.

Additionally, if FeathersJS applications expose multiple service layers—such as authentication services, user profile services, and administrative services—and do not enforce consistent authorization logic across them, a valid JWT may permit traversal across these layers. A compromised or overly permissive JWT scope (e.g., missing proper claims or using a long-lived token) can amplify the impact, allowing an authenticated session to reach sensitive services that should have been isolated. This highlights the importance of coupling JWT validation with explicit, per-request authorization logic that checks resource ownership and application-specific rules rather than relying on the token alone to enforce boundaries.

Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes

Remediation focuses on ensuring that a valid JWT is not treated as sufficient authorization for every operation. In FeathersJS, combine JWT authentication with hooks that enforce ownership and role-based checks. Below are concrete code examples demonstrating secure patterns.

1. Configure authentication with JWT and attach user data to the context

Ensure your authentication setup validates the JWT and enriches the connection context with user information, but does not skip authorization elsewhere.

// src/authentication.js
const { AuthenticationService, JWTStrategy } = require('@feathersjs/authentication');
const { express } = require('@feathersjs/express');

const authentication = new AuthenticationService({
  entity: 'users',
  service: 'users',
  authStrategies: ['jwt'],
  cookie: false
});

authentication.use('jwt', new JWTStrategy());

module.exports = {
  authentication
};

2. Enforce ownership in service hooks

Use before hooks to verify that the authenticated user’s ID matches the resource ID for user-specific endpoints. This prevents unauthorized access even with a valid JWT.

// src/hooks/ensure-ownership.js
module.exports = function ensureOwnership(options = {}) {
  return async context => {
    const { user } = context.params;
    // context.params.user is set by authentication hook from JWT payload
    if (!user) {
      throw new Error('Unauthenticated');
    }

    // For an individual resource request (e.g., GET /users/123)
    if (context.id !== undefined) {
      if (String(context.result._id || context.result.id) !== String(context.params.user._id)) {
        throw new Error('Forbidden: insufficient permissions');
      }
    }

    // For queries (e.g., GET /users) ensure users only see their own records
    if (context.params.query) {
      context.params.query[options.userIdField || 'userId'] = context.params.user._id;
    }

    return context;
  };
};

3. Apply the hook to a user service

Register the ownership hook on the user service to scope records to the authenticated user.

// src/services/users/users.service.js
const { Service } = require('feathersjs');
const ensureOwnership = require('../../hooks/ensure-ownership');

class UserService extends Service {
  // Optional: override find to ensure scoping by default
  async find(params = {}) {
    // Ensure query is scoped to the requesting user if not already restricted
    if (!params.query.userId && params.user && params.user._id) {
      params.query.userId = params.user._id;
    }
    return super.find(params);
  }

  async get(id, params = {}) {
    const record = await super.get(id, params);
    // Double-check ownership in get for safety
    if (!record || String(record._id) !== String(params.user._id)) {
      throw new Error('Forbidden');
    }
    return record;
  }
}

module.exports = function () {
  const app = this;
  const options = {
    name: 'users',
    Model: app.get('UserModel'),
    paginate: app.get('paginate')
  };

  // Initialize service
  app.use('/users', new UserService(options));

  const service = app.service('users');
  service.hooks({
    before: {
      all: [ensureOwnership({ userIdField: 'userId' })],
      find: [],
      get: [],
      create: [],
      update: [],
      patch: [],
      remove: []
    }
  });
};

4. Role-based access control in a separate hook

For administrative or sensitive operations, enforce roles in addition to ownership.

// src/hooks/ensure-admin.js
module.exports = function ensureAdmin(context) {
  const { user } = context.params;
  if (!user || !Array.isArray(user.roles) || !user.roles.includes('admin')) {
    throw new Error('Forbidden: admin role required');
  }
  return context;
};

5. Example of a protected admin service hook usage

// src/services/admin/settings.service.js
const settingsService = new Service({ name: 'admin/settings' });

settingsService.hooks({
  before: {
    all: [ensureAdmin]
  }
});

app.use('/admin/settings', settingsService);

By combining JWT-based authentication with explicit ownership and role checks in hooks, FeathersJS applications can avoid sandbox escape scenarios where a valid token is mistakenly treated as full authorization across all resources.

Frequently Asked Questions

Does a valid JWT alone guarantee that a user can only access their own data in FeathersJS?
No. A valid JWT establishes identity but does not enforce authorization. You must implement per-request ownership or role-based checks in hooks to prevent unauthorized access to other users’ data.
Can middleware or route-level guards replace service hooks for JWT authorization in FeathersJS?
Service hooks are the recommended place for enforcing data-level authorization in FeathersJS because they apply consistently across transports (REST, Socket.io) and centralize checks. Route-level guards can reduce risk but may not cover all service invocations, especially real-time channels.