Bola Idor in Feathersjs with Jwt Tokens
Bola Idor in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
BOLA (Broken Level of Authorization) / IDOR occurs when an authenticated subject can access or modify resources that belong to another subject without explicit authorization. In FeathersJS, this commonly manifests in service hooks and custom code that resolve URLs like /users/123/profile or /posts/456/comments without verifying ownership or tenant context. When JWT tokens are used for authentication, the risk increases if the token payload is trusted without additional checks, or if the token is accepted but the application fails to enforce row-level permissions on each request.
FeathersJS typically authenticates via an authentication hook that decodes the JWT and attaches a user object to context.params. If a developer then uses this context.params.user to scope queries only by ID (e.g., app.service('messages').find({ query: { userId: context.params.user.id } })), but also allows client-supplied IDs in the query (such as id in a REST URL like /messages/789) without verifying that userId matches the token’s subject, a BOLA vulnerability exists. The JWT confirms identity but does not enforce authorization boundaries at the resource level.
Consider a FeathersJS service for user settings:
// services/settings/settings.js
class SettingsService {
async get(id, params) {
// BOLA: id is from the URL, e.g., /settings/other-user-id
// No check that id belongs to params.user.sub
return this.app.models.settings.get(id);
}
}
If the route is protected by JWT authentication and the hook attaches params.user, an attacker can simply iterate or guess numeric IDs (or UUIDs) to read or modify another user’s settings. JWT tokens often contain roles or scopes, but if the application does not validate those claims in the context of resource ownership, the token alone is insufficient to prevent IDOR. Common patterns that lead to BOLA with JWT include:
- Using the decoded JWT subject as a filter but still allowing client-controlled IDs in URLs or query parameters without cross-checking ownership.
- Exposing internal IDs (e.g., database primary keys) directly in URLs without ensuring the subject in the JWT matches the resource’s owner or tenant.
- Relying on client-side filtering only (e.g., returning only resources where
userId === params.user.id) while still permitting the client to request a different ID; the server must enforce this on the backend.
In multi-tenant applications, missing tenant ID checks in JWT-validated queries can also lead to horizontal privilege escalation across organizations. FeathersJS middleware runs hooks in sequence; if authorization is deferred to the service implementation and not enforced consistently, BOLA becomes likely even when JWT authentication is correctly configured.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
Remediation centers on ensuring that every data access operation validates that the resource being accessed belongs to the subject identified by the JWT. Do not trust client-supplied IDs alone; enforce ownership or tenant scope at the service layer or via a centralized hook.
One robust pattern is to inject the user’s identifier into the query automatically and reject any client-supplied ID that conflicts. Below is a complete FeathersJS service example using JWT authentication with enforced ownership checks:
// services/messages/messages.hooks.js
const { iff, isProvider } = require('@feathersjs/hooks-common');
const { authenticate } = require('@feathersjs/authentication').hooks;
const { restrictToOwner } = require('feathers-hooks-common');
module.exports = {
before: [
authenticate('jwt'),
iff(
isProvider('external'),
restrictToOwner({
ownerField: 'userId',
ownerExtractor: context => context.params.user.sub,
})
),
],
};
// services/messages/messages.js
class MessagesService {
async find(params) {
// params.query.userId is enforced by the hook; do not accept id from query
const messages = await this.app.models.messages.find({
query: {
userId: params.user.sub, // scope to subject in JWT
$limit: 50,
},
});
return messages;
}
async get(id, params) {
// Enforce ownership on a per-resource basis
const message = await this.app.models.messages.get(id);
if (message.userId !== params.user.sub) {
throw new Error('Not found');
}
return message;
}
}
If you do not use feathers-hooks-common, implement a custom hook that throws an error when the resource’s owner does not match the JWT subject:
// hooks/ensure-ownership.js
module.exports = function ensureOwnership(options = {}) {
return async context => {
const { user } = context.params;
if (!user || !user.sub) {
throw new Error('Unauthenticated');
}
const record = context.result || (context.data ? null : await context.app.service(context.path).get(context.id));
// For find operations, iterate and filter; for get, single check
if (context.result) {
// find
const filtered = context.result.data || context.result;
const allowed = filtered.filter(item => item.userId === user.sub);
context.result.data = allowed;
} else {
// get
if (!record || record.userId !== user.sub) {
throw new Error('Not authorized');
}
}
return context;
};
};
For applications using UUIDs or non-sequential IDs, ensure the JWT subject is compared against a stable owner field (e.g., ownerId or userId) stored with the resource. Avoid exposing internal database keys directly in URLs when feasible, or always validate the subject against the key. Configure the JWT strategy to include the subject and roles, and validate token signatures and expiration rigorously. Combine route-level authentication with per-method authorization checks so that even if an ID is guessed, the JWT subject must match the resource owner for access to be granted.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |
Frequently Asked Questions
How does JWT authentication interact with FeathersJS hooks to create BOLA risks?
params.user. If service methods use client-provided IDs without verifying that params.user.sub (or another subject claim) matches the resource’s owner, an authenticated user can traverse IDs and access or modify others’ resources. The JWT confirms identity but does not enforce row-level permissions by itself.