Eine einzige Zeile Code kann Ihr gesamtes Zugriffskontrollmodell zunichtemachen. Übertragen Sie den Request-Body direkt in ein Datenbank-Update, und Sie übergeben dem Client quasi einen Stift, um Ihr Schema umzuschreiben. Das ist Mass Assignment. Es ist kein exotischer Bug oder ein Randfall. Es ist ein Designfehler, der immer dann auftritt, wenn eine API ein Payload als ihre eigene Update-Richtlinie behandelt.

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

Es sieht sauber aus. Es spart Tipparbeit. Aber der Client kontrolliert die Keys. Ein Angreifer kann "role": "admin", "accountId": "someone_else" oder "credit": 99999 zu einem ansonsten gewöhnlichen Profil-Update hinzufügen. Ihre Validierungsschicht prüft vielleicht, ob diese Werte Strings oder Zahlen sind, und sagt, dass sie in Ordnung aussehen. Validität ist jedoch nicht gleich Autorisierung. Ein Benutzer besitzt den Ziel-Datensatz möglicherweise legitim. Das bedeutet nicht, dass er das Recht haben sollte, jedes einzelne Feld darin zu bearbeiten.

Wie Mass Assignment tatsächlich aussieht

Die Gefahr verbirgt sich in der Bequemlichkeit. Frameworks und ORMs machen es trivial, JSON-Keys direkt auf Datenbankspalten abzubilden. Wenn Sie dies tun, sagen Sie der Datenbank, dass sie dem Client vertrauen soll, was geändert werden soll, und nicht nur, wie es geändert werden soll.

Ein Benutzer, der sein Profil aktualisiert, sendet möglicherweise gültige Daten für displayName und bio, schleicht aber gleichzeitig role oder balance mit ein. Wenn Ihr Controller das Objekt einfach weiterleitet, schreibt die Datenbank alles. Die Validierung fängt fehlerhafte Werte ab. Sie fängt jedoch selten bösartige Keys ab. Die Geschäftsregel, die besagt: „Dieser Benutzer darf sein Profil aktualisieren“, wird so zu einer pauschalen Berechtigung für jede Spalte in der Zeile.

Die Lösung ist nicht mehr Validierung. Es ist eine strengere Architektur.

Die drei Kontrollinstanzen

Eine sichere Mutation durchläuft drei separate Prüfungen, bevor sie jemals die Speicherung berührt.

Zugelassene Felder (Allowlist)

Beginnen Sie damit, genau festzulegen, welche Keys Sie überhaupt berücksichtigen. Wenn ein Feld nicht auf der Allowlist steht, lehnen Sie die Anfrage ab oder verwerfen Sie den Key. Dies kehrt die Standardeinstellung um: Neue Datenbankspalten sind nicht beschreibbar, bis ein Entwickler sie explizit freigibt. Schemas wachsen im Laufe der Zeit. Ein Teammitglied fügt ein stripeCustomerId, ein departmentBudget oder ein isVerified-Flag hinzu. Mit einer Allowlist sind diese neuen Spalten automatisch vor Schreibzugriffen durch den Client geschützt. Ohne sie wird jede neue Spalte zu einer versehentlichen API-Angriffsfläche.

Gültige Werte

Sobald Sie wissen, welche Felder zulässig sind, prüfen Sie, ob die Werte sinnvoll sind. Ist der Timezone-String tatsächlich eine anerkannte Zeitzone? Ist die E-Mail korrekt formatiert? Liegt die Zahl in einem vernünftigen Bereich? Dies ist reine Hygiene. Es verhindert, dass Müll in Ihr System gelangt, stoppt aber keinen Missbrauch. Ein perfekt valider "admin"-String ist im Feld role immer noch gefährlich, wenn die falsche Person ihn sendet.

Autorisierte Änderungen

Dies ist die Instanz, die die meisten Teams überspringen, und hier liegt der eigentliche Schutz. Stellen Sie eine granulare Frage: Hat dieser spezifische Akteur die Berechtigung, genau dieses spezifische Feld in diesem spezifischen Datensatz zu ändern? Nicht: „Ist der Benutzer ein Admin?“ Nicht: „Hat der Benutzer den write:users-Scope?“ Sondern: „Darf dieser Benutzer seinen eigenen displayName ändern, aber niemals seine accountId?“ Eine feldbezogene Autorisierung verhindert, dass eine breite Berechtigung wie „Editor“ oder „User“ zu einem Generalschlüssel für jede Eigenschaft in der Zeile wird.

Erstellung der Patch-Funktion

Verknüpfen Sie die drei Kontrollinstanzen zu einer einzigen Pipeline. Wenn ein Patch-Request eintrifft, lassen Sie ihn der Reihe nach durch die Phasen laufen.

Erstens: Filtern Sie die Eingabe gegen Ihre Allowlist. Wenn role kein erlaubtes Feld für diesen Endpoint ist, stoppen Sie sofort. Es gibt keinen Grund, einen Wert zu validieren oder zu autorisieren, den Sie niemals hätten erhalten sollen.

Zweitens: Validieren Sie die erlaubten Werte. Prüfen Sie Typen, Formate und Geschäftsregeln. Ein Standort-Feld muss ein String sein, der zu einer echten Zeitzone führt. Eine Avatar-URL muss eine gültige URI unter einer bestimmten Länge sein.

Drittens: Autorisieren Sie die Aktion. Verifizieren Sie, dass der Akteur den Ziel-Datensatz besitzt oder genau die Berechtigung besitzt, die für dieses Feld erforderlich ist. Ownership ist ein guter Standard für persönliche Daten, aber einige Felder benötigen dennoch zusätzliche Hürden. Ein Benutzer besitzt vielleicht sein Profil, aber nur ein Abrechnungsadministrator sollte taxRegion ändern dürfen.

Viertens: Normalisieren Sie die Daten. Entfernen Sie Leerzeichen, reduzieren Sie mehrfache Leerzeichen, wandeln Sie E-Mails in Kleinschreibung um oder entfernen Sie Steuerzeichen. Tun Sie dies nach der Validierung, aber vor der Speicherung, damit Sie während der Autorisierungsprüfungen keine „unsauberen“ Strings vergleichen.

If the input fails any gate, reject the entire mutation. Do not partially apply the safe fields and quietly drop the bad ones. A mixed response trains clients to spray every key they can think of and see what sticks. Fail explicitly.

The Edge Cases That Actually Matter

Mass assignment defenses live or die on details that unit tests often miss.

Duplicate JSON keys. Attackers can send payloads like {"role": "user", "role": "admin"}. Depending on your HTTP parser and framework, the second value might overwrite the first before your application code sees the object. Test this behavior at the parser level. If your framework silently accepts the last key, your allowlist might be looking at "user" while the database receives "admin".

Nested objects, nulls, and arrays. Do not assume the payload is flat. A client might wrap a restricted field inside a nested object like { "profile": { "role": "admin" } }. Your allowlist must recurse if your schema does. Likewise, decide how you handle null. Does it mean "ignore this field" or "delete this field"? And if an array is expected, does your validator reject unexpected structures, or does it cast a single object into an array and let it through?

Unicode normalization. Two strings can look identical to a human while being different sequences of bytes. A user might send a precomposed é or a decomposed e plus combining accent. If your authorization check normalizes once but your storage layer normalizes differently, you can end up with inconsistent data or, worse, a bypass where a username collision slips past your logic. Normalize early and normalize consistently.

Race conditions. Authorization decisions are not freeze frames. They happen at a point in time. Two requests can read the same record, both see that the actor is allowed to write, and both issue updates. In between, the state or the actor’s permissions might have changed. Always apply database updates with a condition on a version number or a state machine value. Use something like UPDATE users SET ... WHERE id = ? AND version = 5. If the row changed since you read it, the write fails. Handle the failure by retrying or rejecting. This keeps stale authorization checks from corrupting your data.

Monitor What Matters

You cannot secure what you cannot see. Build your audit logging around the decision, not just the action.

Log the actor ID and the target ID. Log the exact field names that were accepted and the ones that were rejected. Log the policy version that made the decision and the final result. If a user suddenly starts getting role rejected in their profile update, you want to know immediately.

Never log bearer tokens. Never dump entire request bodies into your logs. An audit trail should help you investigate abuse, not become a repository of credentials and personal data.

The Only Rule You Need

A request body proposes data. It never defines its own authority. The client can ask for anything. Your server decides, field by field and row by row, what is allowed to land in permanent storage. Build your patches with that separation in mind, and mass assignment becomes a problem you stopped long before it reached your authorization layer.