Insecure Direct Object Reference in Adonisjs with Mutual Tls
Insecure Direct Object Reference in Adonisjs with Mutual Tls
Insecure Direct Object Reference (IDOR) occurs when an API exposes a reference to an internal object—such as a numeric ID or UUID—and allows an authenticated subject to act on resources they do not own. In AdonisJS, this commonly surfaces in controller methods that look up models directly from request parameters without enforcing ownership or authorization checks. For example, a route like GET /projects/:id may fetch a project by its primary key, but if the controller does not verify that the authenticated user is a member of that project, any authenticated user can iterate through IDs and access others’ data.
When mutual TLS (mTLS) is used, the server validates the client certificate, but it typically only establishes identity (who you are) rather than authorization (what you are allowed to do). AdonisJS can integrate mTLS via Node.js TLS options, where the framework reads the client certificate from the request socket. While mTLS reduces the risk of spoofed clients, it does not prevent IDOR: once the client certificate is accepted, the application still must enforce per-request authorization. An attacker with a valid certificate—obtained through provisioning or compromise—can still manipulate resource identifiers to access data belonging to other authorized subjects. This creates a combination where strong transport-layer identity coexists with insufficient object-level checks.
Consider an AdonisJS controller that retrieves a user’s documents using a numeric documentId:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Document from 'App/Models/Document'
export default class DocumentsController {
public async show({ params, auth }: HttpContextContract) {
const document = await Document.findOrFail(params.id)
// Missing check: does the authenticated user own this document?
return document
}
}
If the route is protected by mTLS, the client certificate ensures the request comes from a trusted client, but the controller never confirms that the authenticated user (derived from the certificate subject or an attached token) has permission to view this specific document. An attacker who can obtain or guess another user’s document ID can read it. The mTLS layer does not map certificates to permissions, so the vulnerability persists.
IDOR also intersects with other checks in middleBrick’s 12 scans. For instance, BOLA/IDOR and Property Authorization tests look for missing ownership enforcement, while Input Validation checks whether identifiers are properly constrained and sanitized. In AdonisJS, you should treat every user-supplied identifier as untrusted, even when mTLS provides client identity.
To mitigate IDOR in this context, combine mTLS identity with explicit authorization checks. Map the certificate subject or associated user to the resource before returning data, and apply framework-level policies. Avoid using raw database IDs in URLs when possible, or enforce scope checks that tie the resource to the authenticated principal. MiddleBrick’s OpenAPI/Swagger analysis can help identify endpoints where path parameters lack corresponding authorization logic, especially when spec definitions do not align with runtime behavior.
Mutual Tls-Specific Remediation in Adonisjs
Remediation focuses on ensuring that mTLS-derived identity is integrated with AdonisJS authorization logic. You should not rely on mTLS alone to prevent IDOR. Instead, explicitly link the certificate subject or mapped user to the resource being accessed and enforce ownership or role-based checks.
First, configure AdonisJS to extract identity from the client certificate. In your HTTPS server setup, pass TLS options and read the peer certificate:
// start/hooks.ts
import { defineConfig } from '@adonisjs/core/app'
import { join } from 'path'
export default defineConfig({
https: {
key: join(__dirname, '../cert/server.key'),
cert: join(__dirname, '../cert/server.crt'),
ca: join(__dirname, '../cert/ca.crt'),
requestCert: true,
rejectUnauthorized: true,
},
})
In your controller, resolve the authenticated user from the certificate subject (e.g., email or a mapping field) and verify ownership before proceeding:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Document from 'App/Models/Document'
import User from 'App/Models/User'
export default class DocumentsController {
public async show({ params, request }: HttpContextContract) {
const clientCert = request.$ssl?.client?.cert
if (!clientCert) {
throw new Error('Client certificate required')
}
const subject = clientCert.subject
// Example mapping: subject email to user
const user = await User.findBy('email', subject.CN)
if (!user) {
throw new Error('User not found for certificate')
}
const document = await Document.findOrFail(params.id)
// Enforce ownership: ensure document belongs to user
if (document.userId !== user.id) {
throw new Error('Unauthorized: document does not belong to user')
}
return document
}
}
Alternatively, use AdonisJS middleware to centralize this check. Create an auth_mtls middleware that attaches the user to the authentication context and then apply policy checks in a dedicated policy class:
// middleware/verify_mtls.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import User from 'App/Models/User'
export default async function verifyMtls({ request, auth }: HttpContextContract) {
const clientCert = request.$ssl?.client?.cert
if (!clientCert) {
return false
}
const subject = clientCert.subject
const user = await User.findBy('email', subject.CN)
if (!user) {
return false
}
await auth.login(user)
return true
}
Then in your route definition:
import Route from '@ioc:Adonis/Core/Route'
import VerifyMtls from 'App/Middleware/verify_mtls'
import DocumentsController from 'App/Controllers/Http/DocumentsController'
Route.get('/documents/:id', DocumentsController.show)
.middleware(VerifyMtls)
For policy-based enforcement, define a policy that checks ownership:
import { BasePolicy } from '@ioc:Adonis/Core/Policy'
import Document from 'App/Models/Document'
export default class DocumentPolicy {
public async view(user: User, document: Document) {
if (user.id !== document.userId) {
throw new Error('Unauthorized')
}
return true
}
}
Apply the policy in the controller after resolving the model:
import Document from 'App/Models/Document'
import DocumentPolicy from 'App/Policies/DocumentPolicy'
export default class DocumentsController {
public async show({ params, auth }: HttpContextContract) {
const document = await Document.findOrFail(params.id)
await auth.authorize('view', document, DocumentPolicy)
return document
}
}
These steps ensure that mTLS identity is linked to application-level permissions, closing the gap where IDOR could occur despite strong client authentication.
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 |