HIGH sandbox escapefeathersjsapi keys

Sandbox Escape in Feathersjs with Api Keys

Sandbox Escape in Feathersjs with Api Keys — how this specific combination creates or exposes the vulnerability

A sandbox escape in a FeathersJS service that uses API keys occurs when authorization checks are incomplete or inconsistent, allowing an authenticated API key to access or modify data that should be restricted to another scope or tenant. FeathersJS does not enforce tenant boundaries by default; developers must add explicit checks in hooks and services. If API key validation only confirms the key is valid but does not verify scope, tenant ID, or required roles, an attacker can leverage a valid key to traverse these implicit boundaries.

Consider a multi-tenant Feathers service where each record includes a tenantId. A common vulnerable pattern is to query records without scoping to the tenant associated with the API key:

// services/messages/messages.class.js
const { Service } = require('feathersjs');
class MessageService extends Service {
  async find(params) {
    // Vulnerable: no tenantId filtering
    return super.find(params);
  }
  async get(id, params) {
    return super.get(id, params);
  }
  async create(data, params) {
    return super.create(data, params);
  }
}
module.exports = function (app) {
  const options = { name: 'messages', Model: app.get('knex') };
  app.use('/messages', new MessageService(options));
};

An API key issued for tenant tenant_a can be used to request /messages?$limit=100 and potentially read or create records belonging to tenant_b if the query does not filter by tenantId. This violates the principle of least privilege and can lead to data exposure across tenant boundaries, similar to Insecure Direct Object Reference (IDOR) or Broken Level of Authorization (BOLA).

Another vector involves service-level permissions where an API key is allowed to call a service that should be restricted to a specific role or context. For example, a key with administrative-like claims might invoke a generic user service to list all users if the hook does not enforce stricter checks:

// services/users/users.hooks.js
const { iff, isProvider, preventChanges } = require('feathers-hooks-common');
const globalHooks = require('../../hooks');
module.exports = {
  before: {
    all: [],
    find: [iff(isProvider('external'), globalHooks.apiKeyAuthenticate)],
    get: [iff(isProvider('external'), globalHooks.apiKeyAuthenticate)],
    create: [iff(isProvider('external'), globalHooks.apiKeyAuthenticate)],
    update: [iff(isProvider('external'), globalHooks.apiKeyAuthenticate)],
    patch: [iff(isProvider('external'), globalHooks.apiKeyAuthenticate)],
    remove: [iff(isProvider('external'), globalHooks.apiKeyAuthenticate)],
  },
};

If globalHooks.apiKeyAuthenticate only validates the key existence and does not enforce tenant or role constraints, the service may be invoked in contexts that lead to unauthorized data access, effectively enabling a sandbox escape across logical boundaries enforced by the application’s data model.

These issues are detectable by middleBrick’s BOLA/IDOR and Property Authorization checks, which correlate API key usage patterns with expected access boundaries defined by the OpenAPI spec and runtime responses. When a spec-defined scope or tenant filter is missing from a service endpoint that handles sensitive data, middleBrick reports a high-severity finding with remediation guidance to scope queries by the authenticated subject’s tenant or role.

Api Keys-Specific Remediation in Feathersjs — concrete code fixes

To remediate sandbox escape risks when using API keys in FeathersJS, enforce tenant and role checks directly in hooks and ensure service queries are always scoped. Do not rely on API key validation alone for authorization boundaries.

1) Scope data access by tenantId in service hooks:

// services/messages/messages.hooks.js
const { iff, isProvider, preventChanges } = require('feathers-hooks-common');
const globalHooks = require('../../hooks');
function restrictToTenant() {
  return (context) => {
    const { apikey } = context.params; // assume apikey contains tenantId
    if (context.params.provider === 'external' && apikey && apikey.tenantId) {
      // Ensure the query is scoped to the tenant associated with the key
      if (!context.params.query.tenantId) {
        context.params.query.tenantId = apikey.tenantId;
      } else if (context.params.query.tenantId !== apikey.tenantId) {
        throw new Error('Unauthorized: tenant mismatch');
      }
    }
    return context;
  };
}
module.exports = {
  before: {
    all: [],
    find: [iff(isProvider('external'), restrictToTenant())],
    get: [iff(isProvider('external'), restrictToTenant())],
    create: [iff(isProvider('external'), restrictToTenant())],
    update: [iff(isProvider('external'), restrictToTenant())],
    patch: [iff(isProvider('external'), restrictToTenant())],
    remove: [iff(isProvider('external'), restrictToTenant())],
  },
};

2) Validate and normalize API key metadata early in the authentication hook, ensuring tenantId and roles are available for downstream checks:

// hooks/index.js
const apiKeyStore = new Map(); // example in-memory store; use a secure backend in production
function apiKeyAuthenticate(context) {
  const { apikey } = context.params.headers || {};
  if (!apikey) { throw new Error('API key required'); }
  const keyMeta = apiKeyStore.get(apikey);
  if (!keyMeta || keyMeta.expires < Date.now()) {
    throw new Error('Invalid or expired API key');
  }
  // Attach enriched metadata for use in services and hooks
  context.params.authSubject = { tenantId: keyMeta.tenantId, roles: keyMeta.roles || [] };
  context.params.apikey = keyMeta;
  return context;
}
module.exports = { apiKeyAuthenticate };

3) In services, avoid relying on client-supplied query filters for tenant scoping; default to the authenticated subject’s tenantId when absent:

// services/messages/messages.class.js
const { Service } = require('feathersjs');
class MessageService extends Service {
  async find(params) {
    const { apikey } = params;
    const tenantId = apikey && apikey.tenantId ? apikey.tenantId : params.query.tenantId;
    if (!tenantId) { throw new Error('Tenant ID is required'); }
    // Enforce tenant scoping at the query level
    params.query = params.query || {};
    params.query.tenantId = tenantId;
    return super.find(params);
  }
  async get(id, params) {
    const { apikey } = params;
    const tenantId = apikey && apikey.tenantId ? apikey.tenantId : params.query.tenantId;
    if (!tenantId) { throw new Error('Tenant ID is required'); }
    const record = await super.get(id, params);
    if (record.tenantId !== tenantId) {
      throw new Error('Unauthorized: tenant mismatch');
    }
    return record;
  }
}
module.exports = function (app) {
  const options = { name: 'messages', Model: app.get('knex') };
  app.use('/messages', new MessageService(options));
};

These changes ensure that API keys are not treated as a global pass but are tied to explicit authorization checks that scope access by tenantId and roles, reducing the risk of sandbox escape across logical boundaries.

Frequently Asked Questions

How does middleBrick detect a sandbox escape involving API keys in FeathersJS?
middleBrick runs BOLA/IDOR and Property Authorization checks that correlate API key metadata with expected access boundaries defined in your OpenAPI spec and observed runtime behavior. If a service lacks tenantId scoping or role checks while using API keys, the scan flags a high-severity authorization finding with remediation guidance.
Can the free tier of middleBrick scan FeathersJS API key configurations?
Yes, the free tier ($0) allows 3 scans per month. You can submit any public or internally accessible FeathersJS endpoint URL to receive a security risk score and findings, including authorization misconfigurations related to API keys.