Broken Access Control in Feathersjs with Api Keys
Broken Access Control in Feathersjs with Api Keys — how this specific combination creates or exposes the vulnerability
Broken Access Control in FeathersJS when using API keys typically arises from missing or inconsistent authorization checks at the service hook or route level. FeathersJS is a framework that can expose a CRUD-like API surface; if authorization is delegated only to transport layers (e.g., relying on API keys for authentication but not enforcing scope- or role-based checks), an authenticated API key may still be able to access or modify resources it should not.
Consider a Feathers service for user profiles defined with a typical setup:
// src/services/user-profiles/user-profiles.class.js
const { Service } = require('feathersjs');
class UserProfilesService extends Service {
async find(params) {
// Intentionally simplified: no per-call ownership or scope check
return super.find(params);
}
async get(id, params) {
return super.get(id, params);
}
}
module.exports = { UserProfilesService };
If this service is registered and API keys are used purely as a transport authentication mechanism (e.g., via a custom authentication strategy that sets params.user), there is no guarantee that the record owner matches the caller unless explicitly enforced. An attacker who obtains a valid API key (through leakage, insecure storage, or a compromised client) can enumerate IDs via paginated requests or guess predictable IDs (Insecure Direct Object Reference), leading to unauthorized reads or modifications. This maps to OWASP API Security Top 10:2023 — Broken Object Level Authorization (BOLA)/Insecure Direct Object Reference (IDOR).
In Feathers, hooks are the natural place to enforce authorization, but if hooks are omitted, misordered, or bypassed (for example, by registering hooks only for certain transports but not others), the API key authentication alone is insufficient. Another scenario involves property-level authorization: an API key may authenticate identity but should not permit elevation of privileges (BFLA/Privilege Escalation) by allowing a caller to supply parameters such as isAdmin=true that the service inadvertently trusts. Without validating that the authenticated entity is allowed to set or modify sensitive fields, an API key can become a vector for privilege escalation.
Additionally, if the service mixes authentication and authorization concerns or relies on global params.user without scoping queries, you risk data exposure across tenants. For example, a query like find({ query: { userId: params.user.id } }) must be enforced consistently; omitting it or applying it only in select services leaves cross-tenant data accessible to API key holders who should be restricted.
Api Keys-Specific Remediation in Feathersjs — concrete code fixes
Remediation centers on enforcing strict ownership and scope checks within service hooks and ensuring API keys are treated as authentication only, not authorization.
- Use hooks to scope queries to the authenticated entity. For example:
// src/hooks/user-scoped-queries.js
const userScoped = context => {
const { user } = context.params;
if (!user || !user.id) {
throw new Error('Unauthenticated');
}
// Ensure every query is scoped to the requesting user
if (context.params.query) {
context.params.query.userId = user.id;
}
return context;
};
module.exports = { userScoped };
- Register the hook on relevant services:
// src/services/user-profiles/user-profiles.hooks.js
const { userScoped } = require('./hooks/user-scoped-queries');
const { authenticate } = require('@feathersjs/authentication').hooks;
const { protect } = require('@feathersjs/authentication-local').hooks;
module.exports = {
before: {
all: [authenticate('jwt'), userScoped],
find: [protect('id')],
get: [protect('id')]
},
after: {},
error: {}
};
- Validate and restrict mutable fields to prevent privilege escalation. For instance, disallow clients from setting sensitive fields like
isAdminorroleby filtering them in a before hook:
// src/hooks/restrict-sensitive-fields.js
const restrictSensitiveFields = context =>
Promise.resolve().then(() => {
const { user, provider } = context.params;
const data = context.data || {};
if (data.isAdmin !== undefined && (!user || !user.isAdmin)) {
delete data.isAdmin;
}
// Optionally throw if modification is attempted without privilege
return context;
});
module.exports = { restrictSensitiveFields };
- For API key flows, ensure the authentication strategy sets
params.userwith minimal claims and that each service validates ownership explicitly. Example with feathers-authentication and custom API key strategy:
// src/authentication.js
const { AuthenticationService, expressAuthentication } = require('@feathersjs/authentication');
const { iff, preventChanges } = require('@feathersjs/hooks-common');
const apiKeyStrategy = {
name: 'apikey',
authenticate: async (data, config, mainConfig) => {
const { apiKey } = data;
if (!apiKey) throw new Error('An API key is required');
// Look up the key securely; this is illustrative
const record = await mainConfig.app.services.apikeys.get(apiKey);
if (!record || record.revoked) throw new Error('Invalid API key');
return {
user: {
id: record.userId,
scopes: record.scopes || []
}
};
}
};
module.exports = {
service: new AuthenticationService({ entity: 'users', multi: true }),
setup: app => {
app.configure(expressAuthentication({
session: false,
entity: 'users',
key: 'apikey',
userService: app.get('userService'),
authStrategies: [apiKeyStrategy]
}));
}
};
- Apply consistent scoping across services. For example, in a multi-tenant scenario, ensure hooks add tenant ID to queries and reject cross-tenant IDs:
// src/hooks/tenant-scoped.js
const tenantScoped = context =>
Promise.resolve().then(() => {
const { user } = context.params;
if (user && user.tenantId) {
context.params.query.tenantId = user.tenantId;
}
return context;
});
module.exports = { tenantScoped };
These patterns ensure that API keys authenticate identity, but hooks enforce tenant and ownership checks, mitigating BOLA/IDOR and BFLA risks.