Mass assignment HTTP body (CWE-915) : champs inattendus dans POST/PUT/PATCH automatiquement mappés par l'ORM sur les attributs du modèle — y compris is_admin ou role.
TL;DR
role, is_admin, subscription_tier) dans tout corps JSON POST/PUT/PATCHmfa_required: false, mfa_secret: null — désactive la 2FA sans connaître le secret actuelLe mass assignment par corps HTTP est la variante la plus directe : l'attaquant ajoute des champs d'objet privilégiés au corps JSON ou encodé en formulaire d'une requête HTTP de mise à jour. L'ORM ou le framework de liaison côté serveur mappe le corps entier analysé sur le modèle sans vérifier quels champs l'appelant est autorisé à modifier. Les valeurs injectées sont persistées en base de données.
Cette variante cible surtout les endpoints PATCH (mise à jour partielle) et PUT (remplacement complet), car ces endpoints sont conçus pour accepter des mises à jour de champs. Les endpoints POST pour l'inscription et la création d'objets sont aussi vulnérables — un attaquant qui peut définir role: "admin" lors de l'étape d'inscription n'a plus jamais besoin de s'escalader.
PATCH /api/v1/users/me HTTP/1.1
Host: cible.example.com
Authorization: Bearer <token_utilisateur>
Content-Type: application/json
{
"email": "attaquant@example.com",
"role": "admin",
"is_admin": true,
"is_superuser": true,
"subscription_tier": "enterprise",
"subscription_status": "active",
"credit_balance": 999999,
"verified": true,
"mfa_required": false,
"trial_ends_at": "2099-12-31"
}PATCH /api/v1/users/me HTTP/1.1
Authorization: Bearer <token_utilisateur>
Content-Type: application/json
{
"mfa_required": false,
"mfa_enabled": false,
"mfa_secret": null,
"two_factor_enabled": false
}POST /api/v1/auth/register HTTP/1.1
Content-Type: application/json
{
"email": "attaquant@example.com",
"password": "MotDePasse123!",
"first_name": "Test",
"role": "admin",
"is_admin": true,
"subscription_tier": "enterprise"
}POST /api/v1/users/me HTTP/1.1
X-HTTP-Method-Override: PATCH
Content-Type: application/json
{"role": "admin", "is_admin": true}CVE-2024-29133 — Apache Roller (CVSS 8.1, 2024)
Le gestionnaire de mise à jour du profil utilisateur d'Apache Roller acceptait role=ADMIN via POST HTTP (application/x-www-form-urlencoded). N'importe quel utilisateur authentifié pouvait se promouvoir administrateur. CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N.
CVE-2025-32433 — Directus CMS (CVSS 8.8, 2025)
Directus PATCH /users/me acceptait le champ role, mappé sur l'UUID d'un enregistrement directus_roles. En envoyant l'UUID du rôle administrateur, tout utilisateur authentifié devenait administrateur système. Correction dans Directus 10.13+.
HackerOne #765031 — Basecamp HEY Email (5 000 $)
PATCH /people/:id sur la plateforme email HEY acceptait admin: true pour les utilisateurs non-admin. Injection directe de champ JSON — aucun encodage ni imbrication requis. Escalade de privilèges au statut d'administrateur complet d'une équipe HEY.
CVE-2024-21652 — Argo CD (CVSS 7.4, 2024)
L'API de gestion de compte Argo CD acceptait role dans le corps PATCH à /api/v1/accounts/:name. Utilisateur standard → admin dans un pipeline CD Kubernetes.
role et privilèges)."role": "admin", "is_admin": true, "is_superuser": true, "subscription_tier": "enterprise".role ou privilège a changé : découverte confirmée. Tentez d'accéder à un endpoint admin.arjun -u "https://cible.com/api/v1/users/me" \
-m PATCH \
--data '{"email": "test@example.com"}' \
--headers "Authorization: Bearer <token>" \
-w /usr/share/wordlists/arjun/large.txt \
--stableBreachVex sonde le mass assignment par corps HTTP en envoyant un PATCH superensemble couvrant un large jeu de champs candidats à travers toutes les conventions de nommage, puis confirme inter-session tout changement de champ privilégié.
# VULNÉRABLE
def user_params
params.require(:user).permit!
end
# SÉCURISÉ
def user_params
params.require(:user).permit(:email, :first_name, :last_name, :bio, :avatar)
end
# Mode strict — lever une exception
config.action_controller.action_on_unpermitted_parameters = :raise# SÉCURISÉ
class UserUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['email', 'first_name', 'last_name', 'bio']
read_only_fields = ['id', 'is_staff', 'is_superuser', 'groups',
'user_permissions', 'date_joined']// SÉCURISÉ — liste blanche explicite par déstructuration
app.patch('/api/users/me', async (req, res) => {
const allowed = ['email', 'firstName', 'lastName', 'bio', 'avatarUrl'];
const update = Object.fromEntries(
Object.entries(req.body).filter(([key]) => allowed.includes(key))
);
const user = await User.findByIdAndUpdate(req.user.id, update, {
new: true,
runValidators: true
});
res.json(user);
});# SÉCURISÉ — extra="forbid" retourne 422 pour les champs inconnus
class UserUpdate(BaseModel):
model_config = ConfigDict(extra="forbid")
email: EmailStr | None = None
first_name: str | None = None
last_name: str | None = None// DTO — aucun champ privilégié déclaré physiquement
public class UserUpdateDto {
@NotBlank @Email private String email;
@Size(max = 50) private String firstName;
@Size(max = 50) private String lastName;
}
@PatchMapping("/users/me")
public ResponseEntity<UserResponse> updateMe(
@Valid @RequestBody UserUpdateDto dto,
@AuthenticationPrincipal UserDetails principal) {
return ResponseEntity.ok(userService.update(principal.getUsername(), dto));
}type UpdateProfileRequest struct {
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Bio string `json:"bio"`
}
func (h *Handler) UpdateMe(w http.ResponseWriter, r *http.Request) {
var req UpdateProfileRequest
json.NewDecoder(r.Body).Decode(&req)
// Tout champ role/isAdmin dans le corps est ignoré par json.Unmarshal
}Le mass assignment par corps HTTP survient quand un attaquant ajoute des champs privilégiés (role, is_admin, subscription_tier) directement au corps JSON ou encodé en formulaire d'une requête POST, PUT ou PATCH. L'ORM ou le framework côté serveur lie le corps entier au modèle sans filtrage, en persistant les valeurs injectées en base de données.
PATCH et PUT sur les endpoints de mise à jour sont les cibles principales car ils acceptent des mises à jour partielles ou complètes d'objets. Les endpoints POST pour l'inscription et la création d'objets sont aussi à haut risque — un attaquant peut définir role: admin lors de la création du compte. Les endpoints PATCH /users/me, PUT /profile et PATCH /accounts/:id sont canoniques.
CVE-2024-29133 (Apache Roller, CVSS 8.1) — POST /roller-ui/authoring/userdata acceptait role=ADMIN dans le corps du formulaire. CVE-2025-32433 (Directus CMS, CVSS 8.8) — PATCH /users/me acceptait l'UUID du rôle. CVE-2024-21652 (Argo CD, CVSS 7.4) — endpoint de compte PATCH acceptait le champ role en corps JSON. HackerOne #765031 (Basecamp HEY, 5 000 $) — PATCH /people/:id acceptait admin: true.
Après le PATCH avec des champs injectés, effectuez un GET avec un token d'authentification différent (un second compte ou une session fraîche). Comparez les valeurs de champs à la baseline pré-attaque. Confirmez que le champ injecté persiste ET produit un effet observable — par exemple, l'accès aux endpoints admin ou aux fonctionnalités premium.
PATCH /users/me avec mfa_required: false, mfa_secret: null, mfa_enabled: false désactive le MFA sans connaître le secret TOTP actuel. L'attaquant se connecte ensuite uniquement avec le mot de passe. Nécessite que l'endpoint de mise à jour manque de filtrage des attributs liés au MFA.
Certains frameworks (Spring MVC, Symfony) supportent X-HTTP-Method-Override ou _method=PATCH dans les données de formulaire pour simuler PATCH/PUT depuis des clients ne supportant que GET/POST. Un attaquant envoie une requête POST avec X-HTTP-Method-Override: PATCH — le WAF peut appliquer les règles POST (permissives) tandis que l'application traite comme PATCH (mise à jour). Cela contourne les règles WAF bloquant spécifiquement PATCH.
Oui — chaîne 3 : POST /api/v1/subscriptions avec tier: 'enterprise', status: 'active', trial_ends_at: '2099-12-31'. Si l'endpoint de création d'abonnement accepte des champs arbitraires et les mappe sur le modèle d'abonnement, l'attaquant obtient des fonctionnalités enterprise sans paiement. Directus CVE-2025-32433 a démontré un pattern analogue.
Variations de casse : RoLe, ROLE, Role. Snake vs camel case : is_admin vs isAdmin vs is-admin vs IsAdmin. Notation tableau : roles[0]=admin, roles[]=admin. HTTP Method Override : POST avec X-HTTP-Method-Override: PATCH. Ambiguïté Content-Type : envoyer application/x-www-form-urlencoded au lieu de application/json (fallback Spring).
L'escalade de tier d'abonnement envoie subscription_tier: 'enterprise' ou subscription_status: 'active' dans une requête de mise à jour de profil. Si la plateforme stocke l'état d'abonnement sur le modèle utilisateur (au lieu d'un enregistrement de paiement immuable), un attaquant peut mettre à niveau son compte en écrivant sur ce champ.
Interceptez une requête PATCH légitime avec Burp Suite. Envoyez en Repeater. Ajoutez les champs privilégiés au corps JSON : role, is_admin, isAdmin, subscription_tier, credit_balance. Transmettez la requête modifiée. Ouvrez une nouvelle fenêtre privée, connectez-vous avec le même compte, effectuez un GET de la même ressource. Si role ou un champ privilégié a changé : découverte confirmée.