Mass assignment HTTP body (CWE-915) : champs inattendus dans une requête POST/PUT/PATCH mappés par l'ORM aux attributs du modèle, dont is_admin ou role.
role, is_admin, subscription_tier) dans tout corps JSON POST/PUT/PATCHowner_id)mfa_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.
sequenceDiagram
participant A as Attaquant
participant API as PATCH /users/me
participant ORM as User.update(params)
participant DB as Base de données
A->>API: PATCH /users/me {email: "x@y.com", role: "admin", is_admin: true}
API->>ORM: User.update(id, request.body)
Note over ORM: Aucun filtrage — toutes les clés transmises
ORM->>DB: UPDATE users SET email='x@y.com', role='admin', is_admin=true
DB-->>ORM: OK
ORM-->>API: {id: 42, email: "x@y.com", role: "admin"}
API-->>A: HTTP 200 OK
Note over A: GET /admin — accès accordé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}GitHub 2012 — Homakov (sans CVE, sans prime)
Le cas canonique d'injection de corps : un POST vers l'endpoint des clés SSH portait un owner_id pointant sur l'organisation rails/rails, et la clé s'est retrouvée attachée à cette organisation. Aucun encodage spécial, aucune imbrication — le binding du modèle prenait toutes les clés du corps.
CVE-2024-21652 — Argo CD (CVSS 3.1 base 9.8 (NVD), 2024)
Ce n'est pas une faille de liaison de champs : cette CVE est un contournement de la protection anti-brute-force de la connexion, en enchaînant un déni de service et une faiblesse du stockage en mémoire des tentatives échouées (CWE-307), corrigé en 2.8.13, 2.9.9 et 2.10.4. Le 9.8 est la métrique primaire NVD du vecteur CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.
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.
Attention à CVE-2024-21652 (Argo CD, CVSS 3.1 base 9.8 (NVD)) : ce n'est pas une écriture de champ mais un contournement de la protection anti-brute-force de la connexion, par un déni de service enchaîné à une faiblesse du stockage en mémoire des tentatives échouées (CWE-307), corrigé en 2.8.13, 2.9.9 et 2.10.4. Le cas documenté d'injection de corps est GitHub 2012 — un corps POST /keys portait owner_id pointant sur l'organisation rails/rails.
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.
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.