Una sola línea de código puede deshacer todo su modelo de control de acceso. Si vuelca el cuerpo de la solicitud directamente en una actualización de la base de datos, le habrá entregado al cliente un bolígrafo para reescribir su esquema. Eso es la asignación masiva (mass assignment). No es un error exótico ni un caso aislado. Es un fallo de diseño que aparece cada vez que una API trata una carga útil (payload) como si fuera su propia política de actualización.

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

Parece limpio. Ahorra tiempo de escritura. Pero el cliente controla las claves. Un atacante puede añadir "role": "admin", "accountId": "someone_else", o "credit": 99999" a una actualización de perfil que, de otro modo, sería ordinaria. Su capa de validación podría comprobar si esos valores son cadenas o números y decir que parecen estar bien. Sin embargo, la validez no es lo mismo que la autorización. Un usuario puede ser el propietario legítimo del registro de destino, pero eso no significa que deba tener el derecho de editar cada campo dentro de él.

Cómo se ve realmente la asignación masiva

El peligro se esconde en la conveniencia. Los frameworks y los ORM hacen que sea trivial mapear las claves JSON directamente a las columnas de la base de datos. Al hacer esto, le está diciendo a la base de datos que confíe en el cliente sobre qué debe cambiar, no solo sobre cómo debe cambiar.

Un usuario que actualiza su perfil podría enviar datos válidos para displayName y bio, pero incluir role o balance junto a ellos. Si su controlador simplemente reenvía el objeto, la base de datos lo escribirá todo. La validación detecta valores mal formados, pero rara vez detecta claves maliciosas. La regla de negocio que dice "este usuario tiene permiso para actualizar su perfil" se convierte en un permiso general sobre cada columna de la fila.

La solución no es más validación. Es una arquitectura más estricta.

Las tres puertas

Una mutación segura pasa por tres comprobaciones distintas antes de tocar el almacenamiento.

Campos aceptados (Lista de permitidos / Allowlist)

Comience decidiendo exactamente qué claves va a considerar. Si un campo no está en la lista de permitidos, rechace la solicitud o descarte la clave. Esto invierte la postura por defecto: las nuevas columnas de la base de datos no son escribibles hasta que un desarrollador las exponga explícitamente. Los esquemas crecen con el tiempo. Un compañero añade un stripeCustomerId, un departmentBudget o un flag isVerified. Con una lista de permitidos, esas nuevas columnas están protegidas automáticamente contra las escrituras del cliente. Sin ella, cada nueva columna es una superficie de API accidental.

Valores válidos

Una vez que sepa qué campos están permitidos, compruebe si los valores tienen sentido. ¿Es la cadena de la zona horaria realmente una zona horaria reconocida? ¿Tiene el email el formato de un email? ¿Está el número dentro de un rango razonable? Esto es higiene. Evita que entre basura en su sistema, pero no evita el abuso. Una cadena "admin" perfectamente válida sigue siendo peligrosa en el campo role si la envía la persona equivocada.

Transiciones autorizadas

Esta es la puerta que la mayoría de los equipos se salta, y es donde reside la verdadera protección. Haga una pregunta granular: ¿tiene este actor específico permiso para mutar este campo específico en este registro específico? No se pregunte "¿es el usuario un administrador?" ni "¿tiene el usuario el alcance write:users?". Pregunte más bien: "¿tiene este usuario permitido cambiar su propio displayName, pero nunca su accountId?". La autorización por campo evita que un permiso amplio como "Editor" o "Usuario" se convierta en una llave maestra para cada propiedad de la fila.

Construyendo la función de parcheo (Patch)

Integre las tres puertas en un único pipeline. Cuando llegue una solicitud de parcheo (patch), ejecútela a través de las etapas en orden.

Primero, filtre la entrada con su lista de permitidos. Si role no es un campo permitido para este endpoint, deténgase ahí mismo. No hay razón para validar o autorizar un valor que nunca debería haber recibido.

Segundo, valide los valores permitidos. Compruebe tipos, formatos y reglas de negocio. Un campo de ubicación debe ser una cadena que resuelva a una zona horaria real. Una URL de avatar debe ser una URI válida con una longitud determinada.

Tercero, autorice la acción. Verifique que el actor sea el propietario del registro de destino o que posea el permiso exacto requerido para este campo. La propiedad es un buen valor por defecto para los datos personales, pero algunos campos aún necesitan puertas adicionales. Un usuario puede ser dueño de su perfil, pero solo un administrador de facturación debería poder tocar taxRegion.

Cuarto, normalice los datos. Elimine espacios en blanco, colapse espacios repetidos, convierta los emails a minúsculas o elimine caracteres de control. Haga esto después de la validación pero antes del almacenamiento, para no tener que comparar cadenas "sucias" durante las comprobaciones de autorización.

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.