One line of code can undo your entire access control model. Spread the request body straight into a database update and you have handed the client a pen to rewrite your schema. That is mass assignment. It is not an exotic bug or a fringe case. It is a design failure that appears whenever an API treats a payload as its own update policy.

await db.users.update(req.params.id, { ...req.body });

It looks clean. It saves typing. But the client controls the keys. An attacker can add "role": "admin", "accountId": "someone_else", or "credit": 99999" to an otherwise ordinary profile update. Your validation layer might check whether those values are strings or numbers and say they look fine. Validity, however, is not authorization. A user might legitimately own the target record. That does not mean they should have the right to edit every field inside it.

What Mass Assignment Actually Looks Like

The danger hides in convenience. Frameworks and ORMs make it trivial to map JSON keys directly onto database columns. When you do this, you are telling the database to trust the client about what should change, not just how it should change.

A user updating their profile might send valid data for displayName and bio, but slip in role or balance alongside them. If your controller simply forwards the object, the database writes it all. Validation catches malformed values. It rarely catches malicious keys. The business rule that says "this user is allowed to update their profile" becomes a blanket permission over every column in the row.

The fix is not more validation. It is stricter architecture.

The Three Gates

A safe mutation passes through three separate checks before it ever touches storage.

Accepted Fields (Allowlist)

Start by deciding exactly which keys you will even look at. If a field is not on the allowlist, reject the request or drop the key. This flips the default posture: new database columns are non-writable until a developer explicitly exposes them. Schemas grow over time. A teammate adds a stripeCustomerId, a departmentBudget, or an isVerified flag. With an allowlist, those new columns are automatically protected from client writes. Without one, each new column is an accidental API surface.

Valid Values

Once you know which fields are permitted, check whether the values make sense. Is the timezone string actually a recognized timezone? Is the email formatted like an email? Is the number within a sane range? This is hygiene. It stops garbage from entering your system, but it does not stop abuse. A perfectly valid "admin" string is still dangerous in the role field if the wrong person sends it.

Authorized Transitions

This is the gate most teams skip, and it is where the real protection lives. Ask a granular question: does this specific actor have permission to mutate this specific field on this specific record? Not "is the user an admin?" Not "does the user have the write:users scope?" Rather, "is this user allowed to change their own displayName, but never their accountId?" Per-field authorization keeps a broad permission like "Editor" or "User" from becoming a master key to every property in the row.

Building the Patch Function

Tie the three gates into a single pipeline. When a patch request arrives, run it through the stages in order.

First, filter the input against your allowlist. If role is not an allowed field for this endpoint, stop right there. There is no reason to validate or authorize a value you should never have received.

Second, validate the allowed values. Check types, formats, and business rules. A location field must be a string that resolves to a real timezone. An avatar URL must be a valid URI under a certain length.

Third, authorize the action. Verify that the actor owns the target record, or holds the exact permission required for this field. Ownership is a good default for personal data, but some fields still need extra gates. A user might own their profile, yet only a billing admin should touch taxRegion.

Fourth, normalize the data. Trim whitespace, collapse repeated spaces, lowercase emails, or strip control characters. Do this after validation but before storage so you are not comparing dirty strings during authorization checks.

Se l'input fallisce qualsiasi controllo, rifiuta l'intera mutazione. Non applicare parzialmente i campi sicuri scartando silenziosamente quelli errati. Una risposta mista abitua i client a inviare indiscriminatamente ogni chiave possibile per vedere cosa passa. Fallisci in modo esplicito.

I casi limite che contano davvero

Le difese contro la mass assignment dipendono da dettagli che i test unitari spesso trascurano.

Chiavi JSON duplicate. Gli attaccanti possono inviare payload come {"role": "user", "role": "admin"}. A seconda del parser HTTP e del framework utilizzati, il secondo valore potrebbe sovrascrivere il primo prima che il codice dell'applicazione veda l'oggetto. Testa questo comportamento a livello di parser. Se il tuo framework accetta silenziosamente l'ultima chiave, la tua allowlist potrebbe visualizzare "user" mentre il database riceve "admin".

Oggetti annidati, null e array. Non dare per scont