שורת קוד אחת יכולה לבטל את כל מודל בקרת הגישה שלכם. הזרקה ישירה של גוף הבקשה (request body) לעדכון בבסיס הנתונים היא כמו לתת ללקוח עט כדי שיכתוב מחדש את הסכימה (schema) שלכם. זהו Mass Assignment. זה לא באג אקזוטי או מקרה קיצון. זהו כשל תכנוני המופיע בכל פעם ש-API מתייחס ל-payload (מטען נתונים) כאל מדיניות העדכון שלו עצמו.

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

זה נראה נקי. זה חוסך הקלדה. אבל הלקוח שולט במפתחות. תוקף יכול להוסיף "role": "admin", "accountId": "someone_else", או "credit": 99999" לעדכון פרופיל רגיל לחלוטין. שכבת הולידציה שלכם עשויה לבדוק האם הערכים הללו הם מחרוזות או מספרים ולהגיד שהם נראים תקינים. תקינות (Validity), עם זאת, אינה זהה להרשאה (Authorization). משתמש עשוי להיות הבעלים הלגיטימי של הרשומה המבוקשת, אך אין זה אומר שיש לו זכות לערוך כל שדה בתוכה.

איך Mass Assignment נראה בפועל

הסכנה מסתתרת בנוחות. Frameworks ו-ORMs הופכים את המיפוי הישיר של מפתחות JSON לעמודות בבסיס הנתונים לפעולה פשוטה ביותר. כשאתם עושים זאת, אתם אומרים לבסיס הנתונים לסמוך על הלקוח לגבי מה צריך להשתנות, ולא רק לגבי איך הוא צריך להשתנות.

משתמש המעדכן את הפרופיל שלו עשוי לשלוח נתונים תקינים עבור displayName ו-bio, אך להחליק פנימה role או balance לצדם. אם ה-controller שלכם פשוט מעביר את האובייקט הלאה, בסיס הנתונים יכתוב את הכל. וולידציה תתפוס ערכים לא תקינים מבחינה תחבירית, אך היא לעיתים רחוקות תתפוס מפתחות זדוניים. הכלל העסקי שאומר "למשתמש זה מותר לעדכן את הפרופיל שלו" הופך להרשאה גורפת על כל עמודה בשורה.

הפתרון אינו הוספת עוד וולידציה. הוא ארכיטקטורה קשיחה יותר.

שלושת השערים

שינוי (mutation) בטוח עובר שלושה בדיקות נפרדות לפני שהוא נוגע באחסון.

שדות מאושרים (Allowlist)

התחילו בהחלטה בדיוק אילו מפתחות בכלל תסתכלו עליהם. אם שדה אינו נמצא ב-allowlist, דחו את הבקשה או השאירו את המפתח מאחור. זה הופך את גישת ברירת המחדל: עמודות חדשות בבסיס הנתונים הן "לא ניתנות לכתיבה" עד שמפתח יחשוף אותן במפורש. סכימות גדלות עם הזמן. חבר צוות מוסיף stripeCustomerId, departmentBudget, או דגל isVerified. עם allowlist, העמודות החדשות הללו מוגנות אוטומטית מפני כתיבות של הלקוח. ללא כזה, כל עמודה חדשה היא משטח חשיפה (API surface) מקרי.

ערכים תקינים

ברגע שאתם יודעים אילו שדות מותרים, בדקו האם הערכים הגיוניים. האם מחרוזת אזור הזמן היא אכן אזור זמן מוכר? האם האימייל מעוצב כאימייל? האם המספר נמצא בטווח סביר? זהו היגיינה של נתונים. זה מונע מהזבל להיכנס למערכת שלכם, אך זה לא עוצר ניצול לרעה. מחרוזת "admin" תקינה לחלוטין היא עדיין מסוכנת בשדה role אם האדם הלא נכון שולח אותה.

מעברים מורשים (Authorized Transitions)

זהו השער שרוב הצוותים מדלגים עליו, וזה המקום שבו ההגנה האמיתית נמצאת. שאלו שאלה מפורטת: האם לפעולה הספציפית הזו יש הרשאה לשנות את השדה הספציפי הזה ברשומה הספציפית הזו? לא "האם המשתמש הוא אדמין?", ולא "האם למשתמש יש את ה-scope של write:users?". אלא, "האם למשתמש הזה מותר לשנות את ה-displayName שלו, אך לעולם לא את ה-accountId שלו?". הרשאה ברמת השדה מונעת מהרשאה רחבה כמו "עורך" (Editor) או "משתמש" (User) להפוך למפתח מאסטר לכל מאפיין בשורה.

בניית פונקציית ה-Patch

חברו את שלושת השערים לתהליך (pipeline) אחד. כאשר מגיעה בקשת patch, העבירו אותה בשלבים לפי הסדר.

ראשית, סננו את הקלט מול ה-allowlist שלכם. אם role אינו שדה מורשה עבור ה-endpoint הזה, עצרו שם. אין סיבה לבצע וולידציה או אישור לערך שלעולם לא הייתם אמורים לקבל.

שנית, בצעו וולידציה לערכים המאושרים. בדקו טיפוסים, פורמטים וכללים עסקיים. שדה מיקום חייב להיות מחרוזת שמתרגמת לאזור זמן אמיתי. URL של תמונת פרופיל חייב להיות URI תקין תחת אורך מסוים.

שלישית, אשרו את הפעולה (Authorization). ודאו שהפעולה שבוצעה שייכת לבעלים של הרשומה, או מחזיקה בהרשאה המדויקת הנדרשת עבור שדה זה. בעלות היא ברירת מחדל טובה עבור נתונים אישיים, אך חלק מהשדות עדיין זקוקים לשערים נוספים. משתמש עשוי להיות הבעלים של הפרופיל שלו, אך רק מנהל חיובים (billing admin) אמור לגעת ב-taxRegion.

רביעית, נרמלו את הנתונים (Normalization). הסירו רווחים מיותרים, צמצמו רווחים כפולים, הפכו אימיילים לאותיות קטנות (lowercase), או הסירו תווים בקרה. עשו זאת לאחר הוולידציה אך לפני האחסון, כדי שלא תצטרכו להשוות מחרוזות "מלוכלכות" במהלך בדיקות ההרשאה.

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.