Privilege Escalation in Adonisjs with Basic Auth
Privilege Escalation in Adonisjs with Basic Auth — how this specific combination creates or exposes the vulnerability
In AdonisJS, privilege escalation can occur when Basic Authentication is used without enforcing strict role or scope checks after authentication. Basic Auth typically relies on a username and password transmitted via an Authorization header, and AdonisJS can validate credentials using built-in or custom authentication providers. However, if the application only verifies credentials and does not enforce authorization tied to user roles, an authenticated user may access endpoints intended for administrators or other privileged roles.
For example, consider an AdonisJS route guarded by a basic auth check but lacking role-based access control. A low-privilege user who successfully authenticates might still invoke admin-only actions if the route does not explicitly validate permissions. This becomes a BOLA/IDOR-like scenario when object-level ownership is not verified, or a BFLA/Privilege Escalation issue when horizontal access is not restricted. The risk is compounded if the application exposes administrative functionality through predictable URLs without scoping to the authenticated user's role.
AdonisJS provides authentication utilities such as auth middleware and guards, but developers must explicitly assign roles and check them in route handlers or controller methods. Without this, the combination of Basic Auth and missing authorization logic creates a pathway for privilege escalation, where a standard user can perform actions or access data reserved for higher-privilege accounts.
Basic Auth-Specific Remediation in Adonisjs — concrete code fixes
To mitigate privilege escalation in AdonisJS with Basic Auth, implement role-based access control (RBAC) after authentication and validate permissions on every request to sensitive endpoints. Below are concrete code examples demonstrating secure practices.
1. Define a basic auth guard with a custom user provider
Configure an auth guard that uses a database provider to load user details, including roles.
// start/auth.ts
import { defineConfig } from '@ioc:Adonis/Addons/Auth'
export default defineConfig({
guards: {
basic: {
driver: 'basic',
provider: {
driver: 'lucid',
model: () => import('#models/user'),
},
},
},
})
2. User model with role attribute
Ensure your User model includes a role field that determines access levels.
// app/Models/User.ts
import { DateTime } from 'luxon'
import {
BaseModel,
column,
beforeSave,
} from '@ioc:Adonis/Lucid/Orm'
import hash from '@ioc:Adonis/Addons/Hashing'
export default class User extends BaseModel {
@column({ isPrimary: true })
public id: number
@column()
public username: string
@column()
public password: string
@column()
public role: 'user' | 'admin'
@column.dateTime({ autoCreate: true })
public createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime
@beforeSave()
public static async hashPassword(user: User) {
if (user.$dirty.password) {
user.password = await hash.make(user.password)
}
}
}
3. Authenticate and authorize in a route or controller
Use the auth middleware to authenticate, then check the user's role before proceeding.
// start/routes.ts
import Route from '@ioc:Adonis/Core/Route'
import { schema } from '@ioc:Adonis/Core/Validator'
Route.get('/admin/settings', async ({ auth, response }) => {
await auth.authenticate('basic', { guard: 'basic' })
const user = auth.getUser()
if (!user || user.role !== 'admin') {
return response.unauthorized()
}
// Proceed with admin-only logic
return { message: 'Admin settings' }
})
// Example with ownership check (BOLA mitigation)
Route.get('/users/:id/profile', async ({ params, auth, response }) => {
await auth.authenticate('basic', { guard: 'basic' })
const user = auth.getUser()
if (!user) {
return response.unauthorized()
}
// Ensure users can only access their own profile unless admin
if (user.id !== Number(params.id) && user.role !== 'admin') {
return response.forbidden()
}
// Fetch and return profile
return { userId: params.id, role: user.role }
})
4. Centralize authorization with middleware
Create custom middleware to enforce role checks across routes, reducing repetitive logic.
// start/kernel.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { middleware, name } from '@ioc:Adonis/Core/Event'
class RoleMiddleware {
public async handle({ auth, response, request }: HttpContextContract, next: () => Promise, role: 'user' | 'admin') {
await auth.authenticate('basic', { guard: 'basic' })
const user = auth.getUser()
if (!user || user.role !== role) {
return response.unauthorized()
}
await next()
}
}
export const middleware = {
role: RoleMiddleware,
}
export const routeNames = {
role: name('role'),
}
// In routes.ts
Route.get('/admin/users', () => {})
.middleware('role:admin')
.as('admin.users')
5. Validate ownership before sensitive operations
For endpoints that act on specific resources, verify ownership or required privileges to prevent BOLA/IDOR and privilege escalation.
// Example: updating a user record
Route.put('/users/:id', async ({ params, auth, request, response }) => {
await auth.authenticate('basic', { guard: 'basic' })
const user = auth.getUser()
if (!user) {
return response.unauthorized()
}
// Admins can update any user; others can update only themselves
const targetUser = await User.findOrFail(params.id)
if (user.role !== 'admin' && user.id !== targetUser.id) {
return response.forbidden()
}
const payload = request.only(['username', 'role'])
targetUser.username = payload.username
if (payload.role) {
if (user.role !== 'admin') {
return response.forbidden()
}
targetUser.role = payload.role
}
await targetUser.save()
return targetUser
})