Beast Attack in Feathersjs with Api Keys
Beast Attack in Feathersjs with Api Keys — how this specific combination creates or exposes the vulnerability
A Beast Attack (Billion Exploits / BFLA) in a Feathersjs service configured with API keys can occur when an API key provides broad or unverified authorization across multiple resources, and the service does not enforce row-level ownership or scope checks. Feathersjs applications often expose REST or real-time endpoints via hooks and services; if an API key is treated as a fully trusted credential without validating which records a client may access, an attacker can leverage a valid key to iterate through identifiers (IDs, slugs, indexes) and read or modify data that should be isolated.
Consider a Feathersjs service for user documents where authentication is implemented via an API key hook that attaches a static payload like { scope: 'all' } to the request object. If the service’s find and get methods rely only on that payload and do not append a per-entity filter (for example, userId: user.id), an attacker can call /documents?userId=attacker-chosen-id or iterate numeric IDs to enumerate documents they should not see. This is a Beast Attack because the vulnerability scales with the set of accessible identifiers: each valid key may allow enumeration across many resources.
In Feathers, this can be realized when authorization is handled only at the service layer and not enforced through model queries or scope rules. For instance, a service hook that attaches permissions based on the API key might set params.query.userId = payload.userId, but if the hook is missing or incorrectly applied, the raw client-supplied query is passed to the adapter (e.g., Sequelize, Mongoose), which may return records outside the intended scope. An attacker can also probe for IDOR-like patterns—such as changing numeric IDs or UUIDs—to test whether a valid API key grants access to other users’ data. Real-world vectors include endpoints that expose internal IDs, predictable numeric keys, or non-guessable but unindexed identifiers where rate limiting is weak, enabling slow enumeration.
Middleware and framework choices matter here. Feathers hooks that merge params.query without strict sanitization can inadvertently allow clients to override filters. For example, if a client sends userId in the query while the hook also sets userId, behavior may differ depending on merge rules, potentially leaking data. Additionally, services that rely on default adapters may expose relationships that permit traversal across bounded contexts (e.g., accessing related user profiles or permissions) when the API key’s scope is not narrowly defined. Without explicit scoping, each valid API key becomes a potential pivot for accessing a broader dataset, fulfilling the conditions of a Beast/BFLA scenario.
Another contributing factor is the use of unauthenticated or weakly authenticated endpoints (such as those allowing optional API keys) where introspection reveals internal IDs. If an endpoint exposes metadata about records—such as counts or references—and does not enforce row-level checks, attackers can infer the existence of other records and test access. This is especially risky when combined with verbose error messages that distinguish between ‘not found’ and ‘forbidden’, allowing enumeration through timing or status-code differences.
To summarize, the combination of Feathersjs services, API key authentication, and insufficient query scoping enables Beast Attack patterns: a valid key is used to traverse and manipulate a larger set of resources than intended. The risk is elevated when services do not bind access controls to the data layer, when hooks do not sanitize or restrict query parameters, and when predictable or enumerable identifiers are exposed.
Api Keys-Specific Remediation in Feathersjs — concrete code fixes
Remediation centers on ensuring that every API key maps to a constrained scope and that all queries are bound to the principal represented by the key. Do not rely on client-supplied filters alone; enforce ownership or tenant scoping in the service layer and sanitize inputs before they reach the adapter.
Example: a Feathers service for user documents that enforces strict scoping so that API keys cannot access other users’ records.
// src/services/documents/documents.class.js
const { Service } = require('feathers-sequelize');
class DocumentService extends Service {
async find(params) {
// Ensure the query is scoped to the authenticated user’s ID
params.query.userId = params.user.id;
return super.find(params);
}
async get(id, params) {
// Enforce ownership on a per-record basis
params.query = params.query || {};
params.query.userId = params.user.id;
const record = await super.get(id, params);
if (!record || record.userId !== params.user.id) {
throw new Error('Not found');
}
return record;
}
}
module.exports = function (app) {
const options = {
name: 'documents',
Model: app.get('sequelize').models.Document,
paginate: { default: 10, max: 50 }
};
app.use('/documents', new DocumentService(options));
};
Example: an API key hook that attaches a scoped payload and prevents query override by client input.
// src/hooks/api-key-auth.js
module.exports = function apiKeyAuth(options = {}) {
return async context => {
const { headers } = context.params;
const providedKey = headers['x-api-key'] || context.params.query['api_key'];
if (!providedKey) {
throw new Error('Unauthorized');
}
// Validate key against a store (e.g., database, Redis)
const keyRecord = await context.app.service('api-keys').getByKey(providedKey);
if (!keyRecord || keyRecord.revoked) {
throw new Error('Invalid key');
}
// Attach a scoped payload; do not merge client query
context.params.user = {
id: keyRecord.userId,
scope: keyRecord.scope || 'tenant',
tenantId: keyRecord.tenantId
};
// Explicitly restrict query to prevent override
context.params.query = context.params.query || {};
if (!context.params.query.userId) {
context.params.query.userId = keyRecord.userId;
}
return context;
};
};
Example: applying the hook and service with safe merge settings. Ensure hooks run before service methods and that query merges do not allow client values to override security-critical filters.
// src/app.js
const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const apiKeyAuth = require('./hooks/api-key-auth');
const documents = require('./services/documents/documents.class');
const app = express(feathers());
app.configure(express.rest());
app.use('/documents', apiKeyAuth(), documents());
// Disable query override by ensuring hooks set final params.query.userId
app.service('documents').hooks({
before: {
all: [apiKeyAuth()],
find: [context => {
// Explicitly bind userId and disallow client override
context.params.query = context.params.query || {};
context.params.query.userId = context.params.user.id;
return context;
}],
get: [context => {
context.params.query = context.params.query || {};
context.params.query.userId = context.params.user.id;
return context;
}]
}
});
Additional practices: define narrow scopes for each API key (e.g., tenant ID or user ID), validate and sanitize all inputs, avoid exposing internal IDs in URLs where possible, and enforce consistent error handling that does not reveal existence or ownership details. Combine these hooks with server-side validation to ensure that even if a client manipulates a query, the enforced scope remains intact.