Mass Assignment Exploit in Adonisjs
How Mass Assignment Exploit Manifests in Adonisjs
Mass assignment exploits in Adonisjs occur when user-controlled input is directly bound to model properties without explicit allowlisting, enabling attackers to modify sensitive fields like is_admin, role, or balance. Adonisjs uses Lucid ORM, and mass assignment typically happens in controller methods when request.only() or request.except() is omitted, or when request.all() is passed directly to create() or merge().
For example, consider a user registration endpoint in an Adonisjs controller:
// Controllers/UserController.js
async store ({ request, response }) {
const userData = request.all() // DANGEROUS: binds all input fields
const user = await User.create(userData)
return response.created(user)
}
If the User model has a is_admin column, an attacker can send { "email": "[email protected]", "password": "secret", "is_admin": true } to escalate privileges. This maps to OWASP API Security Top 10:2023 API1:2023 – Broken Object Level Authorization (BOLA) when combined with IDOR, but mass assignment itself is a distinct flaw enabling unauthorized property changes.
Another common pattern is during profile updates where developers mistakenly use request.all() without filtering:
// Controllers/ProfileController.js
async update ({ params, request, response }) {
const user = await User.findOrFail(params.id)
user.merge(request.all()) // UNSAFE: allows overwriting any column
await user.save()
return response.ok(user)
}
Here, an attacker could modify email to take over another user's account or set balance in a financial API. Adonisjs does not enable guarded attributes by default, unlike some frameworks, making explicit protection necessary.
Adonisjs-Specific Detection
Detecting mass assignment in Adonisjs requires scanning for patterns where user input is bound to Lucid models without field restriction. middleBrick identifies this by analyzing API endpoints for unsafe data flow from request to model operations. It checks for calls to request.all(), request.only() with insufficient fields, or request.except() missing critical exclusions, followed by Model.create(), model.merge(), or model.fill().
For instance, middleBrick flags endpoints where:
- The request body is not validated via Adonisjs validators (
request.validate()) before model binding - No
$fillableor$guardedarrays are defined in the Lucid model - Input is passed directly to model methods without sanitization
Consider this vulnerable model lacking guards:
// Models/User.js
class User extends Model {
static get table () {
return 'users'
}
// Missing $fillable or $guarded — all columns are mass-assignable
}
module.exports = User
middleBrick correlates runtime behavior (e.g., accepting unexpected fields in POST /users) with static code patterns. It does not require source code access; instead, it sends probing requests with atypical parameters (like is_admin, role) and observes if they are persisted in the response or affect behavior. If a field like is_admin is reflected or leads to privilege changes, it reports a mass assignment finding with severity based on impact (e.g., critical if it enables admin takeover).
This detection aligns with OWASP API Security Top 10 API6:2023 – Unrestricted Access to Sensitive Business Flows, as mass assignment often bypasses intended business logic by altering object state directly.
Adonisjs-Specific Remediation
Fixing mass assignment in Adonisjs involves explicitly defining which attributes are mass-assignable using Lucid’s built-in $fillable or $guarded properties, combined with input validation via Adonisjs validator. Never rely on client-side filtering.
First, define guards in your model. For a User model, specify safe fields:
// Models/User.js
class User extends Model {
static get table () {
return 'users'
}
static get fillable () {
return ['username', 'email', 'password'] // Only allow these
}
// OR use guarded:
// static get guarded () {
// return ['is_admin', 'id', 'created_at', 'updated_at']
// }
}
module.exports = User
With $fillable set, user.merge(request.all()) will ignore is_admin even if sent. However, combining this with explicit validation is stronger:
// Controllers/UserController.js
import { schema, rules } from '@ioc:Adonis/Core/Validator'
async store ({ request, response }) {
const userSchema = schema.create({
email: schema.string({}, [
rules.email(),
rules.unique({ table: 'users', column: 'email' })
]),
password: schema.string({}, [
rules.minLength(8)
])
// Note: is_admin is NOT in schema — rejected if sent
})
const payload = await request.validate({ schema: userSchema })
const user = await User.create(payload)
return response.created(user)
}
This ensures only email and password are processed. The validator strips unknown fields by default when using schema.create() without allowUnknowns. For updates, use similar validation:
async update ({ params, request, response }) {
const user = await User.findOrFail(params.id)
const updateSchema = schema.create({
email: schema.string.optional({}, [rules.email()]),
password: schema.string.optional({}, [rules.minLength(8)])
})
const payload = await request.validate({ schema: updateSchema })
user.merge(payload)
await user.save()
return response.ok(user)
}
middleBrick validates fixes by rescanning the endpoint and confirming that previously accepted sensitive fields (like is_admin) are now ignored or rejected. It reports remediation success when the API no longer reflects unauthorized changes in responses or behavior. This approach prevents OWASP API6 issues by enforcing strict input boundaries at the model and validation layers.