GraphQL : cartographier un schéma par introspection et extraire des données
GraphQL expose une seule route qui répond à des requêtes structurées. Sa force est aussi sa surface d’attaque : le serveur sait décrire son propre schéma via l’introspection. Quand cette fonctionnalité reste active en production, elle livre la carte complète de l’API, y compris des types et des champs qu’aucune interface publique ne montre.
L’introspection, la carte du schéma
Section intitulée « L’introspection, la carte du schéma »GraphQL réserve des champs méta, préfixés par __, pour interroger le schéma lui-même. __schema et __type permettent de reconstruire l’intégralité des types et de leurs champs sans documentation.
La première requête liste tous les types déclarés :
curl -s -X POST "http://api.exemple.fr/graphql" \ -H "Content-Type: application/json" \ -d '{"query":"{__schema{types{name}}}"}' | jqLa réponse contient les types standard (String, Int, Query…) mêlés aux types applicatifs. Un nom qui dénote, absent de l’interface publique, est un candidat direct à approfondir.
Inspecter un type
Section intitulée « Inspecter un type »__type renvoie le détail d’un type nommé : ses champs, leur type et leur nature (kind). En ciblant un type repéré à l’étape précédente, on découvre ce qu’il contient :
curl -s -X POST "http://api.exemple.fr/graphql" \ -H "Content-Type: application/json" \ -d '{"query":"{ __type(name: \"SecretVault\") { name fields { name type { name kind ofType { name } } } } }"}' | jqofType sert à dérouler les types enveloppés, une liste (LIST) ou un champ non nul (NON_NULL), pour remonter jusqu’au type réel du champ.
Repérer le point d’entrée racine
Section intitulée « Repérer le point d’entrée racine »Toutes les requêtes partent du type racine Query. Lister ses champs révèle les points d’entrée réellement exposés à la racine :
curl -s -X POST "http://api.exemple.fr/graphql" \ -H "Content-Type: application/json" \ -d '{"query":"{ __type(name: \"Query\") { fields { name type { name kind } } } }"}' | jqCette requête liste tous les points d’entrée disponibles à la racine. On repère celui dont le type de retour pointe vers le type intéressant, ici SecretVault. Il ne reste plus qu’à utiliser son nom exact pour formuler la requête finale.
Extraire la donnée
Section intitulée « Extraire la donnée »Une fois le point d’entrée et les champs connus, la requête finale sélectionne exactement les champs voulus :
curl -s -X POST "http://api.exemple.fr/graphql" \ -H "Content-Type: application/json" \ -d '{"query":"{ secretVault { secret_id secret_value } }"}' | jqGraphQL retourne précisément les champs demandés. La donnée qui n’apparaissait dans aucune interface est extraite dès lors que le schéma la déclare et qu’aucun contrôle d’accès ne la protège.
Requête brute
Section intitulée « Requête brute »Le même échange en HTTP direct, utile pour rejouer dans un proxy comme Burp :
POST /graphql HTTP/1.1Host: api.exemple.frContent-Type: application/jsonAccept: application/json
{"query":"{__schema{types{name}}}"}Remédiation
Section intitulée « Remédiation »Désactiver l’introspection en production
Section intitulée « Désactiver l’introspection en production »L’introspection est utile en développement, dangereuse en production. La plupart des serveurs permettent de la couper.
// Apollo Server : désactiver introspection hors développementconst server = new ApolloServer({ schema, introspection: process.env.NODE_ENV === 'development',});Couper l’introspection ne suffit pas seul : un attaquant peut deviner des noms de champs par force brute. C’est une mesure de réduction de surface, pas une protection d’accès.
Contrôler l’accès au niveau des résolveurs
Section intitulée « Contrôler l’accès au niveau des résolveurs »Chaque champ sensible doit vérifier l’autorisation dans son résolveur, indépendamment de l’introspection. Un champ ne doit jamais retourner de donnée sur la seule base de sa présence dans le schéma.
const resolvers = { Query: { secretVault: (parent, args, context) => { if (!context.user || !context.user.canReadSecrets) { throw new Error('Non autorisé'); } return getSecretVault(); }, },};Limiter la profondeur et le coût des requêtes
Section intitulée « Limiter la profondeur et le coût des requêtes »GraphQL autorise des requêtes imbriquées arbitrairement profondes, ce qui ouvre un vecteur de déni de service. Une limite de profondeur et un budget de complexité bornent la charge.
// graphql-depth-limit, borne la profondeur d'imbricationvalidationRules: [depthLimit(7)]À retenir
Section intitulée « À retenir »L’introspection GraphQL transforme une API en plan détaillé : __schema liste les types, __type en décrit les champs, et le type Query révèle les points d’entrée racine. Il suffit alors de nommer les bons champs pour extraire la donnée que le schéma déclare. La défense tient en deux temps : réduire la surface en désactivant l’introspection en production, et surtout contrôler l’accès dans chaque résolveur, car un schéma masqué n’est pas un schéma protégé.