HIGH privilege escalationfeathersjsapi keys

Privilege Escalation in Feathersjs with Api Keys

Privilege Escalation in Feathersjs with Api Keys — how this specific combination creates or exposes the vulnerability

FeathersJS is a framework for real-time web applications and REST APIs. When using API keys as an authentication mechanism, privilege escalation can occur when key-based authorization is not scoped or validated with each request. An API key that is treated as a static credential without per-request role or scope checks may allow a lower-privilege key to access endpoints reserved for higher-privilege roles.

Consider a Feathers service that does not validate the key’s associated permissions on every operation. If the service retrieves a key once (for example during authentication) and caches an elevated role in the connection context, subsequent requests using that key may retain the elevated permissions without re-validation. This is a form of BOLA/IDOR when authorization is derived from a cached context rather than from the key itself on each call. Attackers can exploit this by reusing a captured key or by manipulating session or connection state to gain access to administrative endpoints.

Another scenario involves services that rely on global hooks to validate API keys but skip authorization checks in service methods, or incorrectly apply hooks only to create/update/remove but not to custom methods. If a key has write access but a method performs sensitive operations (e.g., modifying roles or changing ownership), and the method does not enforce scope-based checks, a key with seemingly limited permissions can escalate privileges through these unprotected paths. This becomes critical when combined with mass assignment or property-level authorization gaps, where a key can write fields it should not control, including role or permission flags.

Unauthenticated LLM endpoint checks in middleBrick highlight risks when API keys are exposed to AI tooling, but for FeathersJS the concern is that keys used in client-side code or logs may leak into model inputs. If an attacker can trick the system into echoing key-related context into an LLM response, sensitive authorization logic may be revealed, aiding further privilege escalation attempts. Proper scoping and runtime validation reduce the attack surface exposed to such indirect channels.

Real-world attack patterns mirror OWASP API Top 10 2023 broken object level authorization (BOLA) and privilege escalation vectors. For example, a key issued for read-only analytics might be reused to call an admin mutation if the service lacks per-call scope validation. Incorrect $ref handling in OpenAPI specs may also cause generated clients to misrepresent required scopes, leading developers to assume lower privilege when higher privilege is effectively accepted by the endpoint.

Api Keys-Specific Remediation in Feathersjs — concrete code fixes

Remediation centers on ensuring every request validates the API key and its associated scope at the point of use, without relying on cached elevated context. Define key metadata (scopes or roles) at authentication time and enforce checks in service hooks and methods.

Example: Scoped API key validation in a Feathers hook

// src/hooks/require-scoped-key.js
module.exports = function requireScopedKey(requiredScope) {
  return async context => {
    const { app, params } = context;
    const apiKey = params.accessToken || params.apiKey || (params.headers && params.headers['x-api-key']);
    if (!apiKey) {
      throw new Error('Unauthenticated');
    }
    // Fetch key record from a store (e.g., a Feathers service or external DB)
    const keyRecord = await app.service('api-keys').get(apiKey, { paginate: false });
    if (!keyRecord || keyRecord.expires < new Date()) {
      throw new Error('Invalid or expired key');
    }
    if (!keyRecord.scopes || !keyRecord.scopes.includes(requiredScope)) {
      throw new Error('Insufficient scope');
    }
    // Attach validated metadata to context for downstream use
    context.params.key = {
      id: keyRecord.id,
      scopes: keyRecord.scopes,
      owner: keyRecord.owner
    };
    return context;
  };
};

Example: Applying the hook to a Feathers service

// src/services/orders/orders.hooks.js
const requireScopedKey = require('./require-scoped-key');

module.exports = {
  before: {
    all: [],
    find: [ requireScopedKey('orders:read') ],
    get: [ requireScopedKey('orders:read') ],
    create: [ requireScopedKey('orders:write') ],
    update: [ requireScopedKey('orders:write') ],
    patch: [ requireScopedKey('orders:write') ],
    remove: [ requireScopedKey('orders:write') ]
  },
  after: {
    all: [],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  },
  error: {
    all: [],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  }
};

Example: Custom method protection in a Feathers service

// src/services/billing/billing.service.js
class BillingService {
  async patch(id, data, params) {
    const { key } = params;
    if (!key || !key.scopes || !key.scopes.includes('billing:admin')) {
      throw new Error('Admin billing scope required');
    }
    // Ensure row-level ownership if applicable
    const existing = await this.get(id, params);
    if (existing.userId !== key.owner) {
      throw new Error('Forbidden: cannot modify another user billing');
    }
    return this.update(id, data, params);
  }
}

module.exports = function () {
  const app = this;
  const options = {
    name: 'billing',
    paginate: app.get('paginate')
  };
  const service = new BillingService(options);
  service.setup(app);
};

Example: API key definition with scopes in a simple key store service

// src/services/api-keys/api-keys.class.js
class ApiKeysService {
  async create(data, params) {
    const { owner, scopes, expires } = data;
    // Store scopes as an array for precise validation
    return {
      id: `key_${Date.now()}_${Math.random().toString(36).slice(2,8)}`,
      owner,
      scopes: Array.isArray(scopes) ? scopes : [scopes],
      expires: expires ? new Date(expires) : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
    };
  }
}

module.exports = function () {
  const app = this;
  app.use('/api-keys', new ApiKeysService({
    paginate: app.get('paginate')
  }));
};

Operational guidance

  • Always validate the key and its scopes on each request, rather than caching elevated permissions in the connection context.
  • Use per-method hooks to enforce scope requirements and ownership checks, ensuring BOLA and privilege escalation protections are aligned with your authorization model.
  • Store scopes as an array in the key record to support multiple granular permissions; avoid boolean flags like isAdmin that encourage broad privilege escalation.
  • Rotate keys periodically and enforce expiration; treat leaked keys as compromised and revoke immediately.
  • Combine API key checks with property-level authorization where sensitive fields (e.g., role or isAdmin) are user-controllable to prevent mass assignment abuse.

Frequently Asked Questions

Why is per-request scope validation important for API keys in FeathersJS?
Per-request validation prevents privilege escalation by ensuring a key’s scopes are checked on every operation. Caching elevated permissions or skipping validation on certain methods allows lower-privilege keys to access administrative functionality, enabling BOLA and privilege escalation attacks.
How does middleBrick relate to API key privilege escalation in FeathersJS?
middleBrick scans unauthenticated attack surfaces and includes checks that can surface weak authorization around API keys, such as missing scope validation or exposed administrative endpoints. Its findings map to frameworks like OWASP API Top 10 and provide remediation guidance to tighten key-based authorization in FeathersJS.