Identification Failures in Adonisjs (Typescript)
Identification Failures in Adonisjs with Typescript — how this combination creates or exposes the vulnerability
Identification failures occur when an API fails to accurately and reliably confirm the identity of a requestor before granting access to a specific resource. In AdonisJS with TypeScript, this commonly maps to BOLA (Broken Level Authorization) / IDOR findings from middleBrick’s 12 security checks. The framework encourages route‑based resource access where an identifier (e.g., userId, recordId) is supplied by the client, and server‑side code resolves that identifier to a domain model. If the identifier is used without verifying ownership or authorization, an attacker can manipulate numeric or UUID path parameters to access other users’ data.
TypeScript’s static typing can give a false sense of safety. Types are erased at runtime, so a parameter typed as number or string does not enforce correctness or authorization. For example, a route like /users/:id/profile may accept any string or number for id. If the controller resolves the profile by directly substituting id into a database query without confirming that the authenticated actor owns that profile, the endpoint is vulnerable. AdonisJS typically uses Lucid ORM models; if the query scopes are not consistently applied (e.g., missing joins or where‑clauses that enforce tenant or ownership checks), the controller effectively exposes a BOLA/IDOR surface. Even with well‑typed request objects, missing runtime checks allow attackers to iterate through identifiers, enumerate valid resources, and read or modify data they should not see.
Additionally, identification failures can arise from inconsistent or missing authorization checks across related resources. An API might correctly verify that a user can read their own Document, but if a related endpoint such as /documents/:id/comments only validates the document ID without confirming the user’s relation to that document, the comment subresource becomes an indirect identification path. Because AdonisJS routes often nest resources (e.g., documents and comments), developers must enforce authorization at each level. MiddleBrick’s checks for BOLA/IDOR highlight these gaps by correlating spec definitions with runtime behavior, detecting endpoints where identifiers are accepted but not properly bounded by ownership or role‑based constraints.
In TypeScript, interfaces and DTOs can describe shape but not runtime permissions. A controller may accept a strongly typed payload, yet still perform an unfiltered query like Profile.query().where('id', param.id).firstOrFail(). Without an explicit policy that ties the authenticated user’s identity to the query, the type signature is insufficient. The combination of AdonisJS’s convention‑driven resource resolution and TypeScript’s compile‑time typing can obscure runtime authorization gaps, making identification failures a common and high‑severity concern in this stack.
Typescript-Specific Remediation in Adonisjs — concrete code fixes
Remediation centers on enforcing ownership and authorization at runtime, regardless of TypeScript’s static types. Always resolve the authenticated actor and scope queries to that actor explicitly. Use route‑level or controller‑level checks before fetching sensitive resources. Below are concrete TypeScript examples for AdonisJS that demonstrate secure patterns.
1. Scoped query with ownership check
Ensure every data fetch includes a where clause that ties the resource to the authenticated user. Do not rely on the client‑supplied identifier alone.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Profile from 'App/Models/Profile'
export default class ProfilesController {
public async show({ params, auth }: HttpContextContract) {
const profile = await Profile.query()
.where('user_id', auth.user?.id)
.preload('user')
.preload('settings')
.where('id', params.id)
.firstOrFail()
return profile
}
}
2. Policy‑based authorization with explicit checks
Define a policy that decides access, and call it before proceeding. This keeps authorization logic centralized and testable.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Document from 'App/Models/Document'
import DocumentPolicy from 'Policies/DocumentPolicy'
export default class DocumentsController {
public async update({ params, request, auth }: HttpContextContract) {
const document = await Document.query()
.where('id', params.id)
.where('user_id', auth.user?.id)
.firstOrFail()
const policy = new DocumentPolicy(auth, document)
if (!policy.update()) {
throw response.unauthorized('You are not allowed to update this document')
}
// proceed with update
return document.merge(request.only(['title', 'content']))
}
}
3. Avoid exposing raw IDs in URLs when possible; use indirect references
Where feasible, use slugs or scoped identifiers that do not leak global numeric IDs, and always validate scope.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Conversation from 'App/Models/Conversation'
export default class ConversationsController {
public async messages({ params, auth }: HttpContextContract) {
const conversation = await Conversation.query()
.where('user_id', auth.user?.id)
.andWhere('public_id', params.publicId)
.firstOrFail()
// Only fetch messages for the scoped conversation
return conversation.relatedMessages().orderBy('created_at', 'asc').exec()
}
}
4. Centralize authorization logic with middleware or route bindings
Use AdonisJS route bindings or middleware to resolve and verify ownership before reaching the controller.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import User from 'App/Models/User'
export const bindUserById = async (value: string, { auth, response }: HttpContextContract) => {
const user = await User.query().where('id', value).where('id', auth.user?.id).first()
if (!user) {
response.badRequest('Invalid resource access')
}
return user
}
// In routes file:
// Route.get('users/:id/profile', 'ProfilesController.show').bind('id', bindUserById)
5. Validate and sanitize input before using identifiers
Even with strong types, validate that IDs conform to expected formats and are within allowed ranges.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator'
const profileSchema = schema.create({
id: schema.number([
(value) => value > 0,
]),
})
export default class ProfilesController {
public async show({ params, auth, validator }: HttpContextContract) {
const payload = await validator.validate({
schema: profileSchema,
data: { id: params.id },
})
const profile = await Profile.query()
.where('user_id', auth.user?.id)
.where('id', payload.id)
.firstOrFail()
return profile
}
}