Une seule ligne de code peut anéantir tout votre modèle de contrôle d'accès. Injectez directement le corps de la requête dans une mise à jour de base de données, et vous donnez au client un stylo pour réécrire votre schéma. C'est ce qu'on appelle le Mass Assignment. Il ne s'agit pas d'un bug exotique ou d'un cas marginal. C'est un défaut de conception qui apparaît chaque fois qu'une API traite un payload comme sa propre politique de mise à jour.
await db.users.update(req.params.id, { ...req.body });
Cela semble propre. Cela évite de taper du code. Mais le client contrôle les clés. Un attaquant peut ajouter "role": "admin", "accountId": "someone_else", ou "credit": 99999" à une mise à jour de profil par ailleurs ordinaire. Votre couche de validation peut vérifier si ces valeurs sont des chaînes de caractères ou des nombres et conclure qu'elles semblent correctes. La validité, cependant, n'est pas l'autorisation. Un utilisateur peut légitimement posséder l'enregistrement cible. Cela ne signifie pas qu'il doit avoir le droit de modifier chaque champ à l'intérieur de celui-ci.
À quoi ressemble réellement le Mass Assignment
Le danger se cache dans la commodité. Les frameworks et les ORM permettent de mapper très facilement les clés JSON directement sur les colonnes de la base de données. Lorsque vous faites cela, vous dites à la base de données de faire confiance au client sur ce qui doit changer, et pas seulement sur la manière dont cela doit changer.
Un utilisateur mettant à jour son profil peut envoyer des données valides pour displayName et bio, mais y glisser role ou balance en même temps. Si votre contrôleur se contente de transmettre l'objet, la base de données écrit tout. La validation intercepte les valeurs malformées. Elle intercepte rarement les clés malveillantes. La règle métier qui stipule que « cet utilisateur est autorisé à mettre à jour son profil » devient alors une permission globale sur chaque colonne de la ligne.
La solution n'est pas d'ajouter plus de validation. C'est d'avoir une architecture plus stricte.
Les trois barrières
Une mutation sûre doit passer par trois vérifications distinctes avant même de toucher au stockage.
Champs acceptés (Liste d'autorisation)
Commencez par décider exactement quelles clés vous allez examiner. Si un champ ne figure pas sur la liste d'autorisation (allowlist), rejetez la requête ou supprimez la clé. Cela inverse la posture par défaut : les nouvelles colonnes de la base de données ne sont pas modifiables tant qu'un développeur ne les a pas explicitement exposées. Les schémas évoluent avec le temps. Un collaborateur ajoute un stripeCustomerId, un departmentBudget ou un flag isVerified. Avec une liste d'autorisation, ces nouvelles colonnes sont automatiquement protégées contre les écritures du client. Sans elle, chaque nouvelle colonne devient une surface d'exposition accidentelle de l'API.
Valeurs valides
Une fois que vous savez quels champs sont autorisés, vérifiez si les valeurs sont cohérentes. La chaîne de fuseau horaire correspond-elle à un fuseau reconnu ? L'e-mail est-il correctement formaté ? Le nombre se situe-t-il dans une plage raisonnable ? Il s'agit ici d'hygiène. Cela empêche les données erronées d'entrer dans votre système, mais cela n'empêche pas l'abus. Une chaîne "admin" parfaitement valide reste dangereuse dans le champ role si elle est envoyée par la mauvaise personne.
Transitions autorisées
C'est la barrière que la plupart des équipes ignorent, et c'est là que réside la véritable protection. Posez une question granulaire : cet acteur spécifique a-t-il la permission de modifier ce champ spécifique sur cet enregistrement spécifique ? Pas « l'utilisateur est-il un admin ? ». Pas « l'utilisateur possède-t-il le scope write:users ? ». Mais plutôt : « cet utilisateur est-il autorisé à modifier son propre displayName, mais jamais son accountId ? ». Une autorisation par champ empêche une permission large comme « Éditeur » ou « Utilisateur » de devenir une clé maîtresse pour chaque propriété de la ligne.
Construire la fonction de patch
Reliez les trois barrières en un pipeline unique. Lorsqu'une requête de patch arrive, passez-la par les étapes dans l'ordre.
Premièrement, filtrez l'entrée par rapport à votre liste d'autorisation. Si role n'est pas un champ autorisé pour ce point de terminaison (endpoint), arrêtez-vous là. Il n'y a aucune raison de valider ou d'autoriser une valeur que vous n'auriez jamais dû recevoir.
Deuxièmement, validez les valeurs autorisées. Vérifiez les types, les formats et les règles métier. Un champ de localisation doit être une chaîne de caractères qui correspond à un vrai fuseau horaire. Une URL d'avatar doit être une URI valide respectant une certaine longueur.
Troisièmement, autorisez l'action. Vérifiez que l'acteur possède l'enregistrement cible, ou détient la permission exacte requise pour ce champ. La propriété est une bonne valeur par défaut pour les données personnelles, mais certains champs nécessitent des barrières supplémentaires. Un utilisateur peut posséder son profil, mais seul un administrateur de facturation devrait pouvoir toucher à taxRegion.
Quatrièmement, normalisez les données. Supprimez les espaces inutiles, réduisez les espaces répétés, mettez les e-mails en minuscules ou supprimez les caractères de contrôle. Faites cela après la validation mais avant le stockage, afin de ne pas comparer des chaînes de caractères "sales" lors des vérifications d'autorisation.
Si l'entrée échoue à une seule étape de validation, rejetez l'intégralité de la mutation. N'appliquez pas partiellement les champs sûrs en ignorant silencieusement les mauvais. Une réponse mixte apprend aux clients à tester toutes les clés possibles pour voir ce qui passe. Échouez explicitement.
Les cas limites qui comptent vraiment
Les défenses contre l'assignation de masse (mass assignment) dépendent de détails que les tests unitaires ignorent souvent.
Clés JSON en double. Les attaquants peuvent envoyer des payloads comme {"role": "user", "role": "admin"}. Selon votre analyseur HTTP et votre framework, la deuxième valeur pourrait écraser la première avant que votre code applicatif ne voie l'objet. Testez ce comportement au niveau de l'analyseur. Si votre framework accepte silencieusement la dernière clé, votre liste d'autorisation (allowlist) pourrait se baser sur "user" alors que la base de données reçoit "admin".
Objets imbriqués, valeurs nulles et tableaux. Ne supposez pas que le payload est plat. Un client pourrait envelopper un champ restreint dans un objet imbriqué tel que { "profile": { "role": "admin" } }. Votre liste d'autorisation doit être récursive si votre schéma l'est. De même, décidez de la manière dont vous gérez null. Cela signifie-t-il « ignorer ce champ » ou « supprimer ce champ » ? Et si un tableau est attendu, votre validateur rejette-t-il les structures inattendues, ou convertit-il un objet unique en tableau pour le laisser passer ?
Normalisation Unicode. Deux chaînes de caractères peuvent sembler identiques pour un humain tout en étant des séquences d'octets différentes. Un utilisateur peut envoyer un é précomposé ou un e décomposé suivi d'un accent combiné. Si votre contrôle d'autorisation normalise une fois mais que votre couche de stockage normalise différemment, vous pouvez vous retrouver avec des données incohérentes ou, pire, une faille de contournement où une collision de noms d'utilisateur échappe à votre logique. Normalisez tôt et normalisez de manière cohérente.
Conditions de concurrence (race conditions). Les décisions d'autorisation ne sont pas des images figées. Elles se produisent à un instant T. Deux requêtes peuvent lire le même enregistrement, constater toutes deux que l'acteur est autorisé à écrire, et émettre toutes deux des mises à jour. Entre-temps, l'état ou les permissions de l'acteur peuvent avoir changé. Appliquez toujours les mises à jour de la base de données avec une condition sur un numéro de version ou une valeur de machine à états. Utilisez quelque chose comme UPDATE users SET ... WHERE id = ? AND version = 5. Si la ligne a changé depuis votre lecture, l'écriture échoue. Gérez l'échec en réessayant ou en rejetant. Cela empêche les contrôles d'autorisation obsolètes de corrompre vos données.
Surveillez ce qui compte
Vous ne pouvez pas sécuriser ce que vous ne pouvez pas voir. Construisez votre journalisation d'audit autour de la décision, pas seulement de l'action.
Journalisez l'ID de l'acteur et l'ID de la cible. Journalisez les noms exacts des champs qui ont été acceptés et ceux qui ont été rejetés. Journalisez la version de la politique qui a pris la décision et le résultat final. Si un utilisateur commence soudainement à voir son champ role rejeté lors de la mise à jour de son profil, vous devez le savoir immédiatement.
Ne journalisez jamais de jetons (bearer tokens). Ne déversez jamais l'intégralité du corps des requêtes dans vos journaux. Une piste d'audit doit vous aider à enquêter sur les abus, et non devenir un dépôt d'identifiants et de données personnelles.
La seule règle dont vous avez besoin
Le corps d'une requête propose des données. Il ne définit jamais sa propre autorité. Le client peut demander n'importe quoi. Votre serveur décide, champ par champ et ligne par ligne, de ce qui est autorisé à être enregistré dans le stockage permanent. Concevez vos implémentations de PATCH avec cette séparation à l'esprit, et l'assignation de masse devient un problème que vous avez résolu bien avant qu'il n'atteigne votre couche d'autorisation.
