A mass assignment vulnerability is a flaw where the framework copies every field in an incoming request onto an internal object, so a caller can set properties the interface never offered. The form showed four fields. The endpoint accepted nine. One of them was role.
The account registration endpoint of Flowise Cloud, a hosted builder for LLM applications, was meant to take three things: a name, an email, and a password. In April 2026 it was assigned CVE-2026-41267 because it also took everything else.
The published advisory includes the proof of concept. An ordinary registration request carries a user object with a name, an email, and a credential. The attacker's version carries those same three fields, and then keeps going: server-managed metadata such as createdBy and createdDate, and two nested objects the form never had. One is an organization holding an existing organization's UUID. The other is an organizationUser pairing that organization's UUID with the UUID of its owner role.
The account gets created. It arrives already attached to somebody else's organization, holding a role the client picked, and the caller never had to log in. NIST rates it 9.8, critical. It is fixed in version 3.1.0. Nothing in the request was malformed; every value was well-formed and accepted on its merits. The request was simply more complete than the form knew how to be.
That is one advisory. GitHub's advisory database lists fourteen mass assignment advisories against this one project published during 2026, ten of them on a single day in May. They read almost identically: the same flaw in the Variable, Tool, Chatflow, Assistant, CustomTemplate, Dataset, DatasetRow, Evaluation, and Evaluator endpoints, each one letting a resource be reassigned across a workspace boundary. Another, on PUT /api/v1/user, let an authenticated user write the password hash directly and skip password-change verification entirely.
In fairness to the project, that list exists because it files advisories and patches them; plenty of codebases share the habit and produce no paper trail. But the shape of the list is the lesson, and it is worth more than any single CVE in it. Mass assignment is rarely one broken endpoint. It is a convention, adopted once, that every handler in the codebase then inherits.
Key takeaways
- Mass assignment is not an input validation problem. Every value in the request can be perfectly well-formed and the bug still fires.
- The vulnerable line is usually the shortest one in the handler: the convenience call that binds a whole request body onto a model in one step.
- The dangerous fields are the ones only the server should ever set:
role,is_admin,verified,balance,price,tenant_id,owner_id. - It hides from testing because every existing test sends the fields the interface sends. Nobody writes the request with the extra field except an attacker.
- It arrives in clusters, not one at a time. Auto-binding is a convention, so once a codebase adopts it, every handler that writes an object inherits the same flaw.
- The fix is an allow list of writable properties, declared server side. A hidden or disabled field in the form is not a control.
What a mass assignment vulnerability actually is
Web frameworks got popular partly by removing tedium, and one of the most tedious things in a web handler is copying request fields onto an object one line at a time. So frameworks offer to do it in bulk. OWASP's Mass Assignment Cheat Sheet describes the feature plainly: frameworks "allow developers to automatically bind HTTP request parameters into program code variables or objects to make using that framework easier on developers."
That binding is the whole feature and also the whole bug. The framework has no way to know which of an object's properties are yours to set and which belong to the system, so unless you tell it, it assumes all of them. Ruby calls it mass assignment. Spring and ASP.NET call it autobinding. In PHP it gets called object injection. CWE-915 gives it the unglamorous formal name: improperly controlled modification of dynamically-determined object attributes.
The distinction worth holding onto is this. An object contains properties the user owns, like their display name, and properties the system owns, like their role. Authorization at the object level asks whether you may touch this record at all. Mass assignment slips underneath that question, because the answer is yes: it is your record. What is missing is a second, finer decision about which properties of your own record you are allowed to write.
That is why OWASP moved it. Mass assignment was its own entry, API6:2019, in the 2019 API Security Top 10. In the 2023 edition it was folded into API3:2023, Broken Object Property Level Authorization, alongside Excessive Data Exposure. The two were merged because they are the same failure pointed in opposite directions: one reads properties it should not return, the other writes properties it should not accept.
From signup form to admin account
Strip away the UUIDs and the multi-tenancy, and here is the same bug in an ordinary application. A signup page collects three fields and posts them. The handler is one line, because the framework made it one line, and the model has more properties than the form does:
POST /api/users
{ "email": "ada@example.com", "name": "Ada", "password": "..." }
// the handler
const user = await User.create(req.body);
// the model
{ email, name, password, role = "user", emailVerified = false, createdAt }
Nobody wrote a bug. The form has no role selector, the documentation mentions none, and every test in the suite posts exactly the three fields the page collects. But the handler does not accept three fields. It accepts whatever arrives:
POST /api/users
{ "email": "ada@example.com", "name": "Ada", "password": "...",
"role": "admin", "emailVerified": true }
Two properties the interface never offered, both defined on the model, both written. The account is created as an administrator and skips email verification on the way in.
Validation asks whether the values are acceptable. Nobody asked whether the fields were.
Update endpoints are usually the richer target, because they run against an object that already exists and already matters. The same extra field on PATCH /api/users/me promotes an account instead of creating one, and on an orders endpoint the interesting property is not role at all. It is price, or status, or discount.
Which frameworks are exposed, and why
Every framework in the table below has a safe way to do this, and most now default to something safer than they once did. Much of that is owed to a single afternoon in 2012.
On March 4 of that year, a developer named Egor Homakov submitted the form for updating your own GitHub public key with one extra parameter in it. As he described in his own write-up, the handler did something close to @key.update_attributes(params[:public_key]), so he added public_key[user_id] set to the Ruby on Rails organization's account ID. The key stayed his. The owner became theirs. He then committed a file to the rails/rails repository so the point could not be missed. Rails moved to strong parameters, where a controller must name the fields it will accept before the model will take them, and the rest of the ecosystem converged on the same idea.
Which makes the useful question not what causes this but why is it still here. Because every framework that added an allow list also kept a way out of it. In March 2025, Camaleon CMS, a Rails application, was assigned CVE-2025-2304, a critical privilege escalation rated 9.4, for calling permit! during a password change. permit! is the opt-out: it marks every parameter as permitted, restoring exactly the behavior the 2012 fix was built to prevent.
So the exposure that remains is not really the framework's. It is the shortcut the framework still permits, more than a decade after it stopped being the default.
| Framework | The convenient call | The allow list |
|---|---|---|
| Rails (Ruby) | User.new(params[:user]) | params.require(:user).permit(:email, :name) |
| Laravel (PHP) | User::create($request->all()) | $fillable on the model, or $guarded |
| Spring MVC (Java) | binding a request straight onto an entity | setAllowedFields() in an @InitBinder, or bind to a DTO |
| ASP.NET Core (C#) | model binding onto an EF entity | [Bind] and [BindNever], or bind to a view model |
| Django REST Framework (Python) | ModelSerializer with fields = '__all__' | an explicit fields list plus read_only_fields |
| Express and Mongoose (Node) | Model.create(req.body) | select the permitted fields before the write |
One trap deserves its own sentence, because it catches people who believe they are already protected. A schema that silently drops unknown fields does not help here. The properties an attacker wants are not unknown to the schema. role is defined, typed, and has a default. It is a first-class part of the model, which is exactly why writing to it works.
The second trap is the block list. Naming the fields you refuse is a rule you have to remember to update every time somebody adds a column, and the failure is silent when you forget. Naming the fields you accept fails the other way: a new property is unwritable until somebody deliberately adds it. OWASP's prevention guidance for API3:2023 lands in the same place, advising teams to avoid functions that automatically bind client input onto objects and to "allow changes only to the object's properties that should be updated by the client."
How to test for it by hand
This one is genuinely pleasant to test, because the application tells you the answer if you ask in the right order.
- Read the object first. Fetch the resource you are about to write to. The response is the property list, and it is usually more generous than the form. Anything the API returns is a candidate; so is anything you can see in the model, the migration, or the serializer if you have the source.
- Pick the server-owned properties. Look for the ones no user should set: role, permissions, is_admin, verified, status, balance, credit, price, discount, owner_id, tenant_id, created_at, id.
- Replay a legitimate write with one extra field. Take a request the interface actually makes, keep every original value valid, and add a single server-owned property. Change one thing at a time so the result is unambiguous.
- Confirm by reading the object back, not by the status code. This is the step people skip and the reason mass assignment gets missed. A 200 does not mean the field was applied, and a 400 does not always mean it was rejected. Fetch the object again and look at the property.
- Then try the variants. Nested form encoding (
user[role]=admin) where the flat JSON version failed. The update endpoint as well as the create endpoint. PUT as well as PATCH. A different content type. Frameworks bind differently across those paths, and so do handlers.
Do this once per object type rather than once per endpoint, and when you find one, go looking for the rest. That is what a list of fourteen near-identical advisories against a single project is telling you. The property list belongs to the model and the binding style belongs to the codebase, so the same extra field is worth trying everywhere that model is written, and the same shortcut is worth grepping for everywhere a handler takes a request body.
Then write the result down as a test. A negative test for mass assignment is short, it asserts the property is unchanged after a request that tried to change it, and unlike a manual pass it keeps working after somebody adds a column six months from now.
Related readingPart of our series on modern application security testing. Start with What You Should Know About Application Security Testing, and read Authentication Is Not Authorization for the object-level version of this problem, where the caller reaches a record that was never theirs at all.
Frequently asked questions
What is a mass assignment vulnerability?
A mass assignment vulnerability is a flaw where a framework binds every field in an incoming request onto an internal object, so a caller can set properties the interface never exposed. Adding a field such as role or is_admin to an otherwise ordinary request writes it, because nothing in the handler declares which fields the client is allowed to send. It is tracked as CWE-915 and also called autobinding or object injection.
What is the difference between mass assignment and BOLA?
BOLA is about the object: a caller reaches a record that belongs to someone else. Mass assignment is about a property inside the object the caller is already entitled to: a caller writes a field of their own record that only the server should set. OWASP now groups mass assignment under API3:2023 Broken Object Property Level Authorization, which covers property-level rather than object-level failures.
Is mass assignment still in the OWASP API Security Top 10?
Yes, under a different name. Mass assignment was its own entry, API6:2019, in the 2019 list. In the 2023 edition it was merged with Excessive Data Exposure into API3:2023, Broken Object Property Level Authorization, because both share the same root cause: no authorization decision at the level of the individual property. It is also still actively exploited: GitHub's advisory database recorded fourteen mass assignment advisories against a single open-source project during 2026 alone.
Does hiding a field in the user interface prevent mass assignment?
No. Hiding a field, disabling it, or marking it read-only affects only the form the browser renders. The endpoint still accepts the field if the handler binds the whole request body onto the object. Nothing about the client constrains what a caller may put in a request, so the allow list has to exist on the server.
How do you test for a mass assignment vulnerability?
Read the object back first to learn its full property list, pick the properties only the server should set, then replay a legitimate create or update request with one of those properties added. Confirm the result by fetching the object again rather than trusting the status code, because a 200 does not prove the field was applied and an error does not prove it was rejected.
Say which fields you meant
The lesson in 2012 was never that Rails was careless. It was that a framework cannot guess which properties of an object belong to the person sending the request, and if you do not tell it, it will assume all of them. That has not changed, which is why the fix has not changed either. Every safe version of this, in every language in the table, is the same sentence written in different syntax: here are the fields a client may write, and everything else is the server's.
What has changed is the number of places you have to say it. An application with one signup form had one handler to get right. An application with a dozen resource types, each with a create and an update endpoint, has two dozen, and they were all written by someone reaching for the same convenient one-liner.
Your form is a suggestion. Your endpoint is the contract. Write the contract down.