Feb 19, 2026·6 min read·10 visits
AdonisJS Lucid mixed internal state flags (like $isPersisted) with user data on the same object instance. Attackers can inject these flags via JSON payloads to trick the ORM into updating existing records instead of creating new ones, bypassing business logic and potentially taking over accounts.
A critical Mass Assignment vulnerability in the AdonisJS Lucid ORM allows attackers to overwrite internal model state flags. By injecting properties like '$isPersisted', attackers can hijack the Active Record lifecycle, turning harmless INSERT operations into malicious UPDATE queries.
ORMs are the software equivalent of a self-driving car. Most of the time, they get you to the grocery store safely while you nap. But occasionally, they confuse a concrete wall for an exit ramp. AdonisJS Lucid is a popular Active Record ORM that—like many of its predecessors—strives to make database interactions feel like native object manipulation.
Here’s the rub: Active Record patterns often mix data (your username, email, password) with state (is this record saved? is it deleted? what were the original values?). They all live on the same object instance. It's crowded in there.
CVE-2026-22814 is what happens when the bouncer at the club (the input validator) assumes that just because someone is wearing a wristband (is an 'own property'), they belong in VIP. In Lucid, this vulnerability allows a remote attacker to manipulate the internal organs of the ORM itself, simply by sending a JSON payload that the developer was too lazy to filter.
To understand this bug, you have to understand how Lucid decides what to save to the database. When you call User.create(request.all()), Lucid takes that input object and merges it into a new Model instance. But it needs to know: "Is this key a database column, or is it garbage?"
In the vulnerable versions, the check was naively simple. The code essentially asked: "Does this model instance have a property with this name?" using this.hasOwnProperty(key).
Here is where the JavaScript prototype chain bites us. Lucid models have internal state properties initialized in the constructor. Properties like $isPersisted (which tracks if the row exists in the DB) or $attributes (the raw data) are own properties of the instance.
So, if an attacker sends "$isPersisted": true, the check model.hasOwnProperty('$isPersisted') returns true. The ORM nods enthusiastically and overwrites its own internal logic flag with the attacker's value. It's like convincing a bank teller you're the manager just because you're wearing a name tag you brought from home.
Let's look at the crime scene. The vulnerability lived in src/orm/base_model/index.ts. The logic for merging values looked something like this:
// VULNERABLE CODE
merge(values: any) {
Object.keys(values).forEach((key) => {
// The fatal flaw: Internal properties satisfy this check
if (this.hasOwnProperty(key)) {
this[key] = values[key]
}
})
}The fix, implemented in commit b007b12b40cc4a033bc06402b2e40d30fc9f3b85, introduces a hardcoded list of "naughty words"—internal properties that should never be touched by mass assignment.
// PATCHED CODE
const INTERNAL_INSTANCE_PROPERTIES = new Set([
'$columns',
'$attributes',
'$isPersisted', // <--- The most dangerous one
'$isDeleted',
// ... others
])
merge(values: any) {
Object.keys(values).forEach((key) => {
// Explicitly block internal props
if (INTERNAL_INSTANCE_PROPERTIES.has(key)) {
return
}
if (this.hasOwnProperty(key)) {
this[key] = values[key]
}
})
}It’s a brute-force solution, but effective. If the key is on the list, it gets dropped, protecting the ORM's internal state from external tampering.
Let's weaponize this. Imagine a standard user registration endpoint. The developer, trusting the framework, writes this:
// POST /register
public async register({ request }: HttpContextContract) {
// Dangerous: Passing all input directly to create
const user = await User.create(request.all())
return user
}The Attack Chain:
id: 1.{
"username": "hacked_admin",
"email": "pwned@evil.com",
"id": 1,
"$isPersisted": true
}The Hijack:
new User().user.id becomes 1.$isPersisted and sets user.$isPersisted = true.save() method is called. It checks if (this.$isPersisted).The Execution: instead of failing on a Primary Key violation (inserting ID 1), the database executes:
UPDATE users SET username = 'hacked_admin', email = 'pwned@evil.com' WHERE id = 1
Congratulations. You just overwrote the admin's credentials on a public registration form.
This isn't just about overwriting data. It's about fundamental logic bypass. The impact depends heavily on what internal flags are exposed.
UPDATEs allows modifying arbitrary records if the ID is guessable.beforeCreate vs beforeUpdate to enforce security checks (e.g., hashing passwords only on create). By masquerading as an existing record, an attacker might bypass these lifecycle hooks entirely.$isDeleted: true on a new record might confuse the application logic, causing crashes or phantom records that exist in the DB but are ignored by the application queries.In a CVSS v4.0 world, this scores an 8.2 (High) because it requires no privileges (PR:N) and attacks the integrity of the system (VI:H) through a very common coding pattern.
The immediate fix is to upgrade @adonisjs/lucid to version 21.8.2 or 22.0.0-next.6. This applies the internal property denylist. However, relying on the ORM to sanitize your input is like relying on your car's airbag to save you while driving blindfolded.
Developer Takeaways:
request.all(): It is the root of all evil. If your model has a isAdmin column and you use request.all(), you are vulnerable to standard mass assignment even without this specific bug.@vinejs/vine or Zod act as a strict bouncer. They strip out anything not explicitly defined in the schema. If $isPersisted isn't in your validation schema, it never reaches the model.// DO THIS
const data = await request.validateUsing(registerValidator)
User.create(data)This vulnerability is a stark reminder: Input validation is not optional, and internal state should never be mixed with public data.
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
@adonisjs/lucid AdonisJS | < 21.8.2 | 21.8.2 |
@adonisjs/lucid AdonisJS | >= 22.0.0-next.0, < 22.0.0-next.6 | 22.0.0-next.6 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-915 |
| Attack Vector | Network |
| CVSS Score | 8.2 (High) |
| Impact | Integrity Violation / Logic Bypass |
| Exploit Status | PoC Available |
| Affected Component | Lucid BaseModelImpl |
Improperly Controlled Modification of Dynamically-Determined Object Attributes (Mass Assignment)