Mass Assignment in Adonisjs with Mutual Tls
Mass Assignment in Adonisjs with Mutual Tls — how this specific combination creates or exposes the vulnerability
Mass assignment in AdonisJS occurs when a controller binds request payload fields directly to a model without filtering which attributes can be set. In AdonisJS this typically happens via merge or model creation methods, for example User.create(request.body) or user.merge(request.body). Without a strict allowlist (e.g., using schema validation or guarded fields), an attacker can supply unexpected keys such as isAdmin or roleId and change privilege or ownership.
Mutual TLS (mTLS) changes how trust is established but does not change application-level data handling. With mTLS, the server validates the client certificate and may associate the certificate subject with a user identity. Once the server maps the authenticated certificate to a user context, it may treat subsequent request data as safe because the transport was authenticated. This creates a risk: developers may mistakenly assume mTLS alone prevents tampering and omit proper attribute filtering, leading to mass assignment over privileged properties.
Consider an endpoint that updates a user profile and relies on mTLS for identity. If the handler merges the entire payload onto the user model, a compromised or malicious client with a valid certificate can modify role, permissions, or tenantId. The vulnerability is not in the TLS handshake but in the unchecked merge. The combination therefore exposes mass assignment because the perceived security of mTLS may reduce scrutiny on input validation and authorization checks, making it easier to overlook missing guards.
An example of unsafe code is:
import User from '#models/user'
export class UpdateProfileController {
async update({ request, auth }: HttpContextContract) {
const user = await auth.getUserOrFail()
// Risk: merges all request fields without filtering
user.merge(request.body())
await user.save()
return user
}
}
Even with mTLS providing client identity, the unchecked merge can allow privilege escalation if the payload includes fields like role or permissions. The mTLS certificate maps to a user, but authorization at the attribute level is still required.
To safely combine mTLS and mass assignment protection, apply strict schema validation or explicit field allowlists before merging, and enforce per-action authorization regardless of transport-layer guarantees.
Mutual Tls-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on two aspects: enforcing mTLS correctly and preventing mass assignment. For mTLS, configure the AdonisJS server to request and verify client certificates. For mass assignment, validate and filter fields explicitly.
Below is an example of setting up mTLS in an AdonisJS HTTP server configuration (available via start/server.ts or environment-driven configuration). This ensures the server requests a client certificate and verifies it against a trusted CA.
// start/server.ts
import { defineConfig } from '@adonisjs/core/app'
import { HttpServerConfig } from '@poppinss/mrm-server'
const serverConfig: HttpServerConfig = {
https: {
key: '/path/to/server-key.pem',
cert: '/path/to/server-cert.pem',
ca: '/path/to/ca-cert.pem',
requestCert: true,
rejectUnauthorized: true
}
}
export default defineConfig({
http: {
server: serverConfig
}
})
On the client side, present a valid certificate signed by the same CA. In Node clients this can be done with an https agent; in browsers this involves supplying a client certificate during the TLS handshake (typically via user selection or application-managed keystores).
For mass assignment protection, use schema validation to create an allowlist. AdonisJS supports Joi and Yup; here is an example using the schema builder in a controller:
import User from '#models/user'
import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
const profileSchema = schema.create({
firstName: schema.string.optional(),
lastName: schema.string.optional(),
email: schema.string.optional([
schema.email(),
]),
// Explicitly exclude role, isAdmin, tenantId, permissions
})
export class UpdateProfileController {
async update({ request, auth, response}: HttpContextContract) {
const user = await auth.getUserOrFail()
const payload = request.validate({ schema: profileSchema })
// Merge only validated fields
user.merge(payload)
await user.save()
return response.ok(user)
}
}
Alternatively, use a route middleware to enforce authorization per action and combine it with mTLS-based identity. For example, a policy can verify that the certificate subject matches the user being acted upon, and the controller can then safely merge a filtered payload:
import { createMiddleware } from '@adonisjs/core/helpers'
export const ensureMatchingUser = createMiddleware(async (ctx, next) => {
const user = await ctx.auth.getUserOrFail()
// Assuming the certificate subject or a custom header maps to userId
const userId = ctx.request.$custom_claims?.sub || ctx.request.param('id')
if (user.id !== Number(userId)) {
ctx.response.unauthorized('Invalid subject mapping')
return
}
await next()
})
// routes.ts
Route.put('/profile/:id').use(ensureMatchingUser)
.controller('ProfileController.update')
With these measures, mTLS provides strong transport identity while the application layer retains strict control over which fields can be mass-assigned, closing the gap that arises when transport security is mistakenly conflated with data authorization.
Related CWEs: propertyAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-915 | Mass Assignment | HIGH |