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.

Eğer girdi herhangi bir denetimden geçemezse, tüm mutasyonu reddedin. Güvenli alanları kısmen uygulamayın ve hatalı olanları sessizce düşürmeyin. Karma bir yanıt, istemcileri akıllarına gelen her anahtarı denemeye ve hangisinin işe yarayacağını görmeye alıştırır. Açıkça hata verin.

Gerçekten Önemli Olan Uç Durumlar

Mass assignment (toplu atama) savunmaları, birim testlerin genellikle gözden kaçırdığı ayrıntılara bağlıdır.

Yinelenen JSON anahtarları. Saldırganlar {"role": "user", "role": "admin"} gibi payload'lar gönderebilir. HTTP ayrıştırıcınıza (parser) ve framework'ünüze bağlı olarak, ikinci değer uygulama kodunuz nesneyi görmeden önce ilkinin üzerine yazabilir. Bu davranışı ayrıştırıcı düzeyinde test edin. Eğer framework'ünüz son anahtarı sessizce kabul ediyorsa, izin listeniz (allowlist) "user" değerine bakarken veritabanı "admin" değerini alıyor olabilir.

İç içe geçmiş nesneler, null değerler ve diziler. Payload'ın düz (flat) olduğunu varsaymayın. Bir istemci, kısıtlanmış bir alanı { "profile": { "role": "admin" } } gibi iç içe geçmiş bir nesne içine yerleştirebilir. Şemanız iç içe geçiyorsa, izin listeniz de özyinelemeli (recurse) olmalıdır. Benzer şekilde, null değerini nasıl ele alacağınıza karar verin. Bu "bu alanı yoksay" mı yoksa "bu alanı sil" mi anlamına geliyor? Ve eğer bir dizi (array) bekleniyorsa, doğrulayıcınız beklenmedik yapıları reddediyor mu, yoksa tek bir nesneyi bir diziye dönüştürüp geçmesine mi izin veriyor?

Unicode normalizasyonu. İki dize, bir insan için özdeş görünebilirken farklı bayt dizileri olabilir. Bir kullanıcı birleşik bir é veya ayrıştırılmış bir e artı birleştirici aksan gönderebilir. Eğer yetkilendirme kontrolünüz bir kez normalleştiriyor ancak depolama katmanınız farklı şekilde normalleştiriyorsa, tutarsız verilerle karşılaşabilir veya daha kötüsü, bir kullanıcı adı çakışmasının mantığınızın yanından sızdığı bir atlatma (bypass) durumuyla karşılaşabilirsiniz. Erken normalleştirin ve tutarlı bir şekilde normalleştirin.

Yarış durumları (Race conditions). Yetkilendirme kararları donmuş kareler değildir. Belirli bir zaman noktasında gerçekleşirler. İki istek aynı kaydı okuyabilir, her ikisi de aktörün yazmaya izni olduğunu görebilir ve her ikisi de güncellemeler yayınlayabilir. Bu sırada durum veya aktörün izinleri değişmiş olabilir. Veritabanı güncellemelerini her zaman bir sürüm numarası veya bir durum makinesi (state machine) değeri koşuluyla uygulayın. UPDATE users SET ... WHERE id = ? AND version = 5 gibi bir şey kullanın. Satır siz okuduktan sonra değiştiyse, yazma işlemi başarısız olur. Başarısızlığı yeniden deneyerek veya reddederek yönetin. Bu, güncelliğini yitirmiş yetkilendirme kontrollerinin verilerinizi bozmasını engeller.

Önemli Olanı İzleyin

Göremediğiniz şeyi güvence altına alamazsınız. Denetim günlüklerinizi (audit logging) sadece eylem etrafında değil, karar etrafında oluşturun.

Aktör ID'sini ve hedef ID'sini kaydedin. Kabul edilen ve reddedilen tam alan adlarını kaydedin. Kararı veren politika sürümünü ve nihai sonucu kaydedin. Eğer bir kullanıcı profil güncellemesinde aniden role alanının reddedildiğini görmeye başlarsa, bunu hemen bilmek istersiniz.

Asla bearer token'ları kaydetmeyin. Tüm istek gövdelerini (request bodies) asla günlüklerinize dökmeyin. Bir denetim izi (audit trail), kötüye kullanımı araştırmanıza yardımcı olmalıdır; kimlik bilgilerinin ve kişisel verilerin bir deposu haline gelmemelidir.

İhtiyacınız Olan Tek Kural

Bir istek gövdesi veri önerir. Kendi yetkisini asla tanımlamaz. İstemci her şeyi isteyebilir. Sunucunuz, kalıcı depolamaya nelerin kaydedilmesine izin verildiğine alan alan ve satır satır karar verir. Yamalarınızı (patches) bu ayrımı akılda tutarak oluşturun; böylece mass assignment, yetkilendirme katmanınıza ulaşmadan çok önce durdurduğunuz bir sorun haline gelir.