Mass assignment (CWE-915, OWASP A04:2021) : modifier des champs objets jamais destinés aux utilisateurs via la liaison automatique ORM — is_admin, role, balance.
TL;DR
200 OK ne prouve rien — toujours diff GET avant/après avec une session fraîcheextra="forbid" — ne jamais passer req.body directement à un modèleLe mass assignment (CWE-915 : Modification incorrectement contrôlée des attributs d'objet déterminés dynamiquement) est une vulnérabilité où un framework web mappe automatiquement les paramètres d'une requête HTTP sur les propriétés d'un objet ou modèle ORM côté serveur. Lorsque le framework n'applique pas de liste blanche explicite des champs accessibles en écriture, un attaquant peut injecter des attributs privilégiés — role, is_admin, subscription_tier, credit_balance — jamais destinés à être contrôlés par l'utilisateur.
L'OWASP API Security Top 10 2023 subsume le mass assignment sous API3:2023 BOPLA (Broken Object Property Level Authorization), couvrant à la fois la direction écriture (mass assignment) et la direction lecture (exposition excessive de données). Selon l'OWASP, cela affecte plus de 30 % des APIs testées en 2025.
Le mass assignment est architecturalement distinct de l'IDOR (Broken Object Level Authorization / BOLA). L'IDOR contrôle les objets auxquels vous pouvez accéder. Le mass assignment contrôle les champs que vous pouvez écrire sur les objets auxquels vous accédez légitimement. Les deux sont fréquemment chaînés : une faiblesse BOLA donne accès à l'objet cible, et une faiblesse BOPLA permet de modifier ses champs privilégiés.
La vulnérabilité a une cause racine unique : l'application passe des données contrôlées par l'utilisateur directement à un appel de liaison ORM sans filtrer préalablement les clés autorisées.
Le flux d'attaque :
PATCH /users/me, PUT /accounts/:id, POST /register.role, is_admin qui semblent en lecture seule.Démonstration minimale :
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_superuser": true,
"subscription_tier": "enterprise",
"credit_balance": 999999
}HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 42,
"email": "attaquant@example.com",
"role": "admin",
"subscription_tier": "enterprise"
}| Variante | Technique | Impact |
|---|---|---|
| Injection dans le corps HTTP | Champs supplémentaires dans un corps JSON POST/PATCH | Escalade de rôle, contournement de facturation |
| Injection de champ JSON | Propriétés JSON non documentées, objets imbriqués | Escalade de privilèges, désactivation MFA |
| Attributs imbriqués Rails | role_attributes, permissions_attributes avec _destroy | Manipulation de modèles associés |
| Input de mutation GraphQL | Champs supplémentaires dans les variables de mutation | Takeover admin dans les headless CMS |
| Pattern spread ORM | {...req.body}, User.create(params) dans Express/Rails | Écrasement complet du modèle |
| JSON Patch (RFC 6902) | op: replace, path: /role sur endpoint PATCH | Écrasement au niveau du champ |
| JSON Merge Patch (RFC 7396) | Valeur null efface les champs ACL | Suppression des permissions |
| Override de méthode HTTP | X-HTTP-Method-Override: PATCH sur POST | Contournement WAF |
| Pollution de prototype | __proto__: {isAdmin: true} via merge Node.js | Escalade de privilèges à l'échelle applicative |
GitHub 2012 — Homakov Mass Assignment Rails
En mars 2012, le chercheur en sécurité Egor Homakov a exploité une vulnérabilité de mass assignment sur GitHub.com en production. En postant une clé SSH avec un paramètre owner_id pointant vers l'organisation rails/rails, il a associé sa clé publique à cette organisation sans autorisation. Cet incident a directement conduit à l'introduction de strong_parameters comme fonctionnalité obligatoire dans Rails 4 (2013).
CVE-2024-52034 — Strapi CMS (CVSS 8.1, 2024)
Strapi v4.x PATCH /api/users/:id acceptait les champs role, confirmed et blocked dans le corps de la requête sans liste blanche explicite. Les utilisateurs authentifiés pouvaient s'escalader en admin en envoyant {"role": "<id-role-admin>"}. Correction dans Strapi 4.25.x. 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 exposait la table directus_users via des endpoints CRUD automatiques avec le champ role accessible en écriture. PATCH /users/me avec {"role": "<uuid-role-admin>"} élevait tout utilisateur authentifié en administrateur système. Correction dans Directus 10.13+. CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N.
CVE-2024-32887 — NocoDB (CVSS 8.8, 2024)
L'endpoint de mise à jour de vue de table NocoDB acceptait les champs role et visibility directement depuis le corps de la requête sans couche DTO. N'importe quel utilisateur authentifié pouvait modifier les permissions au niveau de la table.
Un 200 OK en réponse à un PATCH avec des champs injectés ne confirme pas un mass assignment. Rails, Django et la plupart des frameworks modernes ignorent silencieusement les champs non autorisés. Confirmez chaque découverte en effectuant un GET avec un token de session séparé après le PATCH et en vérifiant que le champ privilégié a persisté. Sans cette confirmation inter-session, la découverte est un faux positif.
PUT /users/:id, PATCH /users/me, PATCH /profile, POST /register.role, is_admin, subscription_tier.PATCH /api/v1/users/me HTTP/1.1
Content-Type: application/json
Authorization: Bearer <token_utilisateur>
{
"email": "vous@example.com",
"role": "admin",
"is_admin": true,
"is_superuser": true,
"subscription_tier": "enterprise",
"mfa_required": false
}InputObject correspondant à la cible.Arjun (v3.x) effectue la découverte de paramètres HTTP en envoyant des requêtes avec des noms de paramètres candidats depuis un dictionnaire et en détectant les différences de réponse.
Burp Suite Pro Param Miner détecte les paramètres non documentés via l'analyse différentielle des réponses. Utilisez-le sur les endpoints PATCH avec un corps JSON.
OpenAPI-fuzzer analyse la spec OpenAPI/Swagger de la cible, identifie les champs marqués readOnly: true et tente automatiquement de les écrire.
BreachVex détecte le mass assignment via plusieurs techniques complémentaires : il envoie une sonde PATCH superensemble avec un large jeu de champs candidats et compare la réponse, confirme la persistance via une session authentifiée distincte, et valide un effet secondaire observable (accès à un endpoint admin ou déblocage de fonctionnalité premium) avant de rapporter un finding.
Rails — strong_parameters :
# VULNÉRABLE — permet tous les champs
def user_params
params.require(:user).permit!
end
# SÉCURISÉ — liste blanche explicite
def user_params
params.require(:user).permit(:email, :first_name, :last_name, :bio)
end
# Mode strict — lever une exception sur les champs non autorisés
config.action_controller.action_on_unpermitted_parameters = :raiseDjango REST Framework — champs explicites :
# VULNÉRABLE
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__'
# SÉCURISÉ
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'email', 'first_name', 'last_name', 'bio']
read_only_fields = ['id', 'is_staff', 'is_superuser', 'groups']Express + Mongoose — déstructuration :
// VULNÉRABLE
const user = await User.findByIdAndUpdate(req.user.id, req.body, { new: true });
// SÉCURISÉ
const { email, firstName, lastName, bio } = req.body;
const user = await User.findByIdAndUpdate(
req.user.id,
{ email, firstName, lastName, bio },
{ new: true, runValidators: true }
);Pydantic v2 / FastAPI — extra="forbid" :
# SÉCURISÉ — les champs supplémentaires retournent 422
class UserUpdate(BaseModel):
model_config = ConfigDict(extra="forbid")
email: EmailStr | None = None
first_name: str | None = None
last_name: str | None = NoneNestJS — ValidationPipe avec liste blanche :
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true
}));def test_user_cannot_escalate_role(client, user_token):
response = client.patch(
"/api/v1/users/me",
json={"email": "user@example.com", "role": "admin", "is_superuser": True},
headers={"Authorization": f"Bearer {user_token}"}
)
me = client.get("/api/v1/users/me", headers={"Authorization": f"Bearer {user_token}"})
assert me.json()["role"] == "user"
assert me.json().get("is_superuser") is not TrueLe mass assignment (CWE-915) survient lorsqu'un framework web mappe automatiquement les paramètres d'une requête HTTP sur les propriétés d'un objet côté serveur sans restreindre les champs que l'utilisateur peut écrire. Un attaquant injecte des champs comme `role: 'admin'` ou `is_admin: true` qui ne devaient pas être contrôlables. L'OWASP API Security Top 10 2023 classe cela sous API3:2023 BOPLA (Broken Object Property Level Authorization).
BOPLA (Broken Object Property Level Authorization) est le nom donné par l'OWASP API Security Top 10 2023 pour API3. Il fusionne deux catégories 2019 : API6 (Mass Assignment, direction écriture) et API3 (Excessive Data Exposure, direction lecture). Si votre API permet à des appelants d'écrire des champs qu'ils ne devraient pas contrôler, c'est du mass assignment. Les deux sont du BOPLA.
L'IDOR (Insecure Direct Object Reference / BOLA) contrôle les objets auxquels un utilisateur peut accéder — par exemple, si l'utilisateur A peut lire /users/2. Le mass assignment (BOPLA) contrôle les champs qu'un utilisateur peut écrire sur des objets auxquels il accède légitimement — par exemple, si la mise à jour de son profil lui permet de définir role=admin. Les deux sont souvent chaînés.
Rails 3 et antérieurs assignaient automatiquement chaque paramètre POST/PUT aux attributs de modèle correspondants. Rails 4+ a introduit les strong_parameters : params.require(:user).permit(:email, :name). Utiliser permit! (tout permettre) ou omettre permit() réactive la vulnérabilité. Rails 8 a introduit params.expect() avec une régression double-bracket pour les hachages imbriqués.
Le ModelSerializer de Django REST Framework avec Meta.fields = '__all__' expose chaque champ du modèle en lecture et en écriture. Un attaquant peut PATCHer is_superuser, is_staff, groups et user_permissions. La solution est une liste explicite de champs et read_only_fields pour les attributs privilégiés.
Le pattern User.findByIdAndUpdate(id, req.body) ou Object.assign(user, req.body) passe le corps JSON complet à l'ORM. Tout champ du corps — y compris role, isAdmin, stripeCustomerId — est écrit en base de données. Correction : déstructurer uniquement les champs autorisés depuis req.body.
CVE notables : CVE-2024-52034 (Strapi CMS, CVSS 8.1), CVE-2025-32433 (Directus CMS, CVSS 8.8), CVE-2024-32887 (NocoDB, CVSS 8.8), CVE-2024-21652 (Argo CD PATCH role, CVSS 7.4), CVE-2024-29133 (Apache Roller, CVSS 8.1), CVE-2025-64459 (Django filter kwargs, CVSS 9.1), CVE-2025-23085 (chaîne NoSQL opérateur Express Node.js, CVSS 7.3), et CVE-2024-43788 (chaîne pollution de prototype webpack, CVSS 7.3).
Non. Les frameworks modernes ignorent silencieusement les champs non autorisés et retournent 200. Une attaque réussie nécessite une confirmation de persistance : après le PATCH, effectuer un GET avec un token de session différent et vérifier que le champ privilégié a changé. Confirmer un effet secondaire observable (accès à un endpoint admin) apporte la preuve définitive.
Oui. Les types d'entrée GraphQL qui exposent role, isAdmin ou permissions dans leur InputObject acceptent ces valeurs via les variables de mutation. Les plateformes headless CMS (Strapi, Directus, Hasura) génèrent des mutations directement depuis le schéma de base de données, rendant chaque colonne accessible en écriture par défaut. Utilisez GraphQL Voyager pour découvrir les champs privilégiés écribles.
JSON Patch (RFC 6902) est un format PATCH utilisant des objets d'opération : [{"op": "replace", "path": "/role", "value": "admin"}]. Les endpoints acceptant Content-Type: application/json-patch+json sans valider les chemins autorisés permettent d'écraser n'importe quel champ JSON, y compris role, permissions et subscription_tier.
Interceptez une requête PATCH ou PUT légitime. Envoyez en Repeater. Ajoutez des champs privilégiés au corps JSON : role, is_admin, isAdmin, subscription_tier, credit_balance. Transmettez et effectuez immédiatement un GET avec une session fraîche pour vérifier la persistance. Utilisez l'extension Param Miner pour découvrir les paramètres non documentés.
Rails 8 a introduit params.expect() comme remplaçant plus strict de params.require().permit(). Avec des hachages imbriqués en forme mono-bracket, Rails autorise incorrectement un tableau de hachages au lieu d'un seul hachage pour les attributs imbriqués, permettant l'injection d'attributs sur les modèles associés. La correction est la forme double-bracket.
La pollution de prototype (CWE-1321) survient quand une charge utile de mass assignment cible __proto__ ou constructor.prototype de JavaScript. Envoyer {"__proto__": {"isAdmin": true}} à un endpoint Node.js utilisant lodash.merge < 4.17.21 pollue le prototype global de Object, faisant retourner true à toutes les vérifications de propriété isAdmin dans le processus.