Missing $fillable declaration permits unintended field mass-assignment in AdonisJS Lucid APIs
0 reputation · 07 Feb 2022, 09:34 UTC
0 reputation · 07 Feb 2022, 09:34 UTC
AdonisJS Lucid models declare mass-assignable fields through an explicit $fillable array; when this property is absent, all public attributes are writable via API payloads. This default, shifted to opt-in status in AdonisJS 5.x, means that models without a defined $fillable expose every column to potential modification through resourceful routes. When route guards or authentication middleware are not uniformly applied, unauthenticated requests may interact with these fields, particularly when combined with the framework's default CORS policy that allows all origins. The resulting overlap of implicit assignability, missing access controls, and permissive cross-origin settings creates a surface area for accidental public exposure that model definitions alone do not resolve.
35275 reputation · 07 Feb 2022, 15:57 UTC
$fillable propertyThe premise of the question contains a category error. AdonisJS Lucid does not implement a $fillable (or fillable) property on models. That pattern belongs to Laravel Eloquent. In Lucid, mass-assignment behavior is controlled by how you call create(), fill(), or merge() and which data you pass to them.
Model.create(payload) and model.fill(payload) accept any plain object and will set every key that matches a defined column or computed property. There is no built-in allowlist/blocklist on the model class.// Controller
const payload = await request.validate({
name: schema.string(),
email: schema.string.email(),
// isAdmin is intentionally omitted
})
await User.create(payload)
If you instead do User.create(request.all()) or user.fill(request.all()), every column present in the request body will be written—not because a $fillable is missing, but because Lucid never had that guard.
$fillable make all attributes mass-assignable?Irrelevant—the property does not exist. All public columns are assignable whenever you pass them to create()/fill()/merge() without filtering.
No. Authentication only verifies who is calling. It does not restrict which fields the caller may send. An authenticated admin or a compromised user token can still overwrite is_admin, password_hash, stripe_customer_id, etc., if the controller passes the raw request body to the model.
CORS is orthogonal. Restricting Access-Control-Allow-Origin limits which browser origins can make credentialed requests, but it does not filter request bodies. A same-origin or allowed-origin request can still contain arbitrary fields. CORS hardening is good practice, but it does not replace input validation.
request.all() directly to Lucid. Use the validator (request.validate()) or request.only([...]) to build an allowlisted payload..create(request.all()), .fill(request.all()), or .merge(request.all()) patterns.@beforeCreate) that throws if unexpected keys are present, but treat this as a safety net, not a primary control.# 1. Confirm AdonisJS version
node ace --version
# 2. Search for unsafe patterns (run from project root)
grep -r "\.create(request\.all\(\))" --include="*.ts" .
grep -r "\.fill(request\.all\(\))" --include="*.ts" .
grep -r "\.merge(request\.all\(\))" --include="*.ts" .
# 3. Verify no $fillable or fillable references exist in your models
grep -r "fillable" --include="*.ts" app/Models/
# Should return zero matches unless you copied Laravel code
AdonisJS 6 (current major as of 2026-10-12) retains the same model API: no $fillable. If a community package or fork adds such a property, it is non-standard and should be documented in that package's README. Always verify against the official Lucid documentation for your exact version.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.