Privilege Escalation in Adonisjs with Api Keys
Privilege Escalation in Adonisjs with Api Keys
In AdonisJS, privilege escalation via API keys commonly occurs when a low-privilege token is accepted by a route that does not enforce scope or role checks, allowing the caller to access or modify higher-privilege resources. AdonisJS does not include built-in API key management; developers typically implement keys via request headers (e.g., X-API-Key) and validate them against a database model. If authorization logic is incomplete—such as using only the key’s existence to authenticate, without verifying scopes or tenant/owner relationships—an attacker who obtains or guesses a key may perform BOLA/IDOR and escalate privileges across users or administrative functions.
Consider an AdonisJS route that retrieves user data by ID without confirming that the requesting API key belongs to the same tenant or has elevated scopes:
// routes.ts
Route.get('/users/:id', async ({ request, auth }) => {
const user = await User.findOrFail(request.param('id'))
return user
}).middleware(['auth:api'])
If the API key middleware only sets a generic user identity without enforcing scope-based authorization, an attacker can increment the :id parameter to access other users’ data. A more privileged key might also allow creation, update, or deletion of resources (BFLA), since the controller does not differentiate between read-only and administrative keys. This pattern aligns with common findings in BOLA/IDOR and Privilege Escalation checks: a valid key is accepted, but the application fails to assert that the key’s associated permissions match the requested action or resource.
In the context of the 12 security checks run by middleBrick, this manifests as a finding when unauthenticated or low-privilege probing reveals that an API key grants access to endpoints or operations that should be restricted to specific roles or scopes. For example, a key issued for read-only billing may still trigger destructive admin endpoints if the server does not validate scopes. Such gaps highlight the importance of binding API keys to explicit permissions and validating those permissions on every request, rather than relying on the key as a simple bearer token.
Api Keys-Specific Remediation in Adonisjs
Remediation centers on ensuring that each API key is tied to a defined scope or role and that every controller enforces these constraints. Below is a concrete approach using AdonisJS middleware and policy checks. First, extend your API key model to include scope attributes:
// app/Models/ApiKey.ts
import { DateTime } from 'luxon'
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
export default class ApiKey extends BaseModel {
@column({ isPrimary: true })
public id: number
@column()
public keyHash: string
@column()
public scope: string // e.g., 'read:billing', 'write:users', 'admin'
@column.dateTime()
public expiresAt: DateTime
@column.dateTime('auto')
public createdAt: DateTime
@column.dateTime('auto')
public updatedAt: DateTime
}
Next, implement a scope validation middleware that checks the key’s scope against the required permission for the route:
// start/hooks.ts
import { Exception } from '@poppinss/utils'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export const validateApiKeyScope = async (ctx: HttpContextContract, next: () => Promise<void>, requiredScope: string) => {
const apiKey = ctx.request.header('x-api-key')
if (!apiKey) {
throw new Exception('API key missing', 401, 'MISSING_API_KEY')
}
const keyRecord = await ApiKey.query()
.where('keyHash', hashKey(apiKey)) // assume a secure hash function
.where('expiresAt', '>', DateTime.local()) // ensure not expired
.preload('scope') // if scopes are a separate relation
.first()
if (!keyRecord) {
throw new Exception('Invalid or expired API key', 401, 'INVALID_API_KEY')
}
if (!hasScope(keyRecord.scope, requiredScope)) {
throw new Exception('Insufficient scope', 403, 'INSUFFICIENT_SCOPE')
}
ctx.auth.user = keyRecord // attach to auth for downstream use
await next()
}
function hasScope(keyScope: string, requiredScope: string): boolean {
// Implement scope hierarchy or exact matching as needed
return keyScope === requiredScope || keyScope === 'admin'
}
Then apply this middleware to routes that require specific scopes, and ensure controllers do not trust implicit permissions:
// routes.ts
Route.get('/users/:id', async ({ request, auth }) => {
const user = await User.findOrFail(request.param('id'))
// auth.user is set by validateApiKeyScope and can be used for ownership checks
return user
}).middleware([() => validateApiKeyScope({}, 'read:users')])
Route.post('/users', async ({ request, auth }) => {
// Only allow if scope includes 'write:users' or 'admin'
const payload = request.only(['name', 'email'])
const user = await User.create(payload)
return user
}).middleware([() => validateApiKeyScope({}, 'write:users')])
For admin operations, require a strict scope and avoid accepting elevated keys on shared endpoints. If you use a package for hashing keys, ensure it is cryptographically strong and that keys are never logged. Combine this with ownership checks (e.g., confirming that a modifying request’s subject matches the authenticated key’s tenant or user ID) to prevent BOLA/IDOR and privilege escalation paths.