Bola Idor in Adonisjs with Api Keys
Bola Idor in Adonisjs with Api Keys — how this specific combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA) occurs when an API exposes one user’s resource through an object identifier (ID) without verifying that the requesting user owns or is allowed to access that resource. In AdonisJS, this commonly appears in route handlers that accept a dynamic parameter (e.g., :id) and directly fetch a database record without confirming the authenticated principal has permission to view or modify it. When API keys are used for authentication, the risk of BOLA is heightened if authorization checks are omitted or incorrectly scoped to the key itself rather than to the resource ownership or tenant.
Consider an AdonisJS route that retrieves a user’s profile using an API key for authentication but fails to assert that the profile belongs to the principal associated with the key:
// routes.ts
Route.get('/profiles/:id', async ({ params, auth }) => {
const profile = await Profile.findOrFail(params.id);
return profile;
});
If the API key is validated via an auth guard that only confirms the key is valid, any caller with a valid key can enumerate arbitrary profile IDs simply by incrementing :id. This is a classic BOLA because the authorization boundary is at the key level, not at the resource level. Attackers may leverage this to enumerate sensitive profiles, export personal data, or chain findings with other endpoints.
AdonisJS supports multiple auth schemes, and using API keys often means implementing a custom guard or leveraging the built-in api guard. If the guard attaches the key to the authentication context but the developer skips a membership or ownership check, BOLA emerges. For example, a key might map to an integration or service account, and the application might assume the key’s scope is sufficient without verifying that the requested resource falls within that scope.
Real-world attack patterns include IDOR enumeration via predictable numeric IDs, accessing other tenants’ data in multi-tenant setups, and abusing weakly enforced relationships (e.g., userId on a profile). Even with API keys, if the handler does not validate that the key’s associated scope or subject aligns with the resource, the endpoint remains vulnerable. This is particularly relevant when keys are long-lived or shared across services, increasing the window for abuse.
In AdonisJS, developers should treat API keys as credentials that establish identity, not as a complete authorization policy. Combining key-based authentication with explicit checks on resource ownership or tenant membership is essential to prevent BOLA. The framework’s policies and abilities system can help, but only if consistently applied to every route that accesses a user-controlled resource.
Api Keys-Specific Remediation in Adonisjs — concrete code fixes
To mitigate BOLA when using API keys in AdonisJS, enforce resource-level authorization in every handler that accesses a user-controlled or tenant-scoped resource. Below are concrete, idiomatic examples that demonstrate secure patterns.
1. Explicit ownership check with a numeric ID
Ensure the authenticated principal’s identifier matches the resource’s owner before returning data:
// routes.ts
Route.get('/profiles/:id', async ({ params, auth }) => {
const profile = await Profile.findOrFail(params.id);
const user = await auth.authenticate(); // retrieves the user associated with the API key
if (profile.userId !== user.id) {
throw new ResponseException('Forbidden', 'You do not own this profile', 403);
}
return profile;
});
2. Policy-based authorization using AdonisJS policies
Define a policy that encapsulates the permission logic and invoke it in the route:
// policies/profile_policy.ts
import { BasePolicy } from '@ioc:Adonisjs/Shield';
export default class ProfilePolicy extends BasePolicy {
public async view(user: User, profileId: number) {
const profile = await Profile.findOrFail(profileId);
return profile.userId === user.id;
}
}
// routes.ts
Route.get('/profiles/:id', async ({ params, auth }) => {
const user = await auth.authenticate();
const canView = await policy('ProfilePolicy').read(user, params.id);
if (!canView) {
throw new ResponseException('Forbidden', 'Access denied', 403);
}
return Profile.findOrFail(params.id);
});
3. Scoped queries that incorporate the key’s tenant or subject
When API keys represent integrations or service accounts, scope queries to the key’s tenant or organization:
// routes.ts
Route.get('/tenant/:tenantId/documents/:documentId', async ({ params, auth }) => {
const user = await auth.authenticate();
const document = await Database.from('documents')
.where('id', params.documentId)
.andWhere('tenant_id', user.tenantId)
.first();
if (!document) {
throw new ResponseException('Not found or insufficient permissions', 403);
}
return document;
});
4. Use route-level middleware to enforce ownership early
Create a lightweight middleware that validates resource ownership before reaching the handler:
// middleware/ensure_ownership.ts
import { Exception } from '@ioc:Adonis/Core/Exception';
export default async function ensureOwnership(request: HttpContextContract) {
const { id } = request.params;
const user = await request.auth.authenticate();
const exists = await Profile.query()
.where('id', id)
.andWhere('user_id', user.id)
.first();
if (!exists) {
throw new Exception('Forbidden', 403);
}
}
// routes.ts
Route.get('/profiles/:id', 'ProfileController.show').middleware(['ensureOwnership']);
These patterns emphasize that API keys should not replace fine-grained authorization. Always couple key validation with explicit checks that tie the key’s scope to the resource being accessed, and prefer policy objects or scoped queries to keep authorization logic centralized and testable.
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 |