Vulnérabilités des API GraphQL¶
GraphQL est un langage de requête pour API conçu pour des échanges efficaces entre client et serveur. Sa particularité : le client spécifie exactement les données qu'il veut dans la réponse, ce qui évite les réponses surdimensionnées et les appels multiples typiques des API REST. GraphQL est agnostique de la plateforme et peut s'interfacer avec n'importe quel type de stockage.
Contrairement à REST, toutes les requêtes sont des POST adressées à un endpoint unique. C'est le type et le nom de l'opération qui déterminent son traitement ; les réponses sont des objets JSON dans la structure demandée.
Concepts fondamentaux¶
Un schéma définit les types manipulés par l'API. Le ! y marque les champs obligatoires :
Une query est l'équivalent d'une lecture (GET en REST) ; une mutation l'équivalent d'une écriture (POST/PUT/DELETE) :
query {
getProduct(id: 123) {
name
description
}
}
mutation {
createProduct(name: "Verre à cocktail", listed: "yes") {
id
name
listed
}
}
Plusieurs éléments de syntaxe reviennent et sont utiles à connaître pour un test :
- Champs (fields) : les objets de données interrogeables. On précise dans la requête les champs que l'on souhaite voir renvoyés.
- Arguments : des valeurs fournies à un champ (
getProduct(id: 1)), définies dans le schéma. - Variables : permettent de passer des arguments dynamiquement, en les déclarant (
$id: ID!) puis en fournissant leurs valeurs dans un dictionnaire JSON séparé. - Alias : un objet ne peut pas contenir deux propriétés de même nom ; les alias contournent cette limite en nommant explicitement chaque retour. Ils permettent surtout de regrouper plusieurs opérations dans une seule requête HTTP — un point central pour le contournement des limites de débit.
- Fragments : des portions réutilisables de query ou de mutation.
- Subscriptions : des connexions durables (souvent via WebSocket) pour recevoir des mises à jour en temps réel.
- Introspection : une fonction qui interroge le serveur sur son propre schéma. À désactiver en production, car elle expose une cartographie complète de l'API.
Trouver l'endpoint¶
La query universelle query{__typename} renvoie, sur tout endpoint GraphQL, une réponse contenant {"data": {"__typename": "query"}} : c'est le test de détection le plus fiable. On la lance contre les chemins courants :
En ajoutant au besoin un suffixe de version (/v1). En pratique, les endpoints GraphQL n'acceptent souvent que des requêtes POST en application/json — ce qui les protège du CSRF. Il vaut donc la peine de retenter la query universelle avec d'autres méthodes (GET) ou un autre type de contenu (x-www-form-urlencoded) : si elles passent, le terrain est propice à d'autres attaques.
Exploiter des arguments non assainis (IDOR)¶
Si l'API accède à des objets directement à partir d'un argument, elle peut être vulnérable à un contrôle d'accès défaillant. Lorsqu'une liste publique omet certains éléments (un produit non listé, par exemple), il suffit souvent de demander explicitement l'identifiant manquant pour y accéder :
Si l'objet masqué de la liste générale est renvoyé par cette requête ciblée, le contrôle d'accès repose à tort sur la simple non-exposition de l'identifiant.
Découvrir le schéma¶
La meilleure approche est l'introspection, en interrogeant le champ __schema disponible à la racine de toute query. Le plus simple est de laisser Burp générer la requête d'introspection ; son scanner la détecte d'ailleurs automatiquement et signale « GraphQL introspection enabled ». Une sonde minimale :
Quand l'introspection est désactivée, plusieurs voies subsistent :
- Les suggestions. La plateforme Apollo propose, dans ses messages d'erreur, des corrections de champ (« Did you mean 'productInformation'? »). L'outil Clairvoyance s'en sert pour reconstituer tout ou partie du schéma. Burp signale ce comportement comme « GraphQL suggestions enabled ».
- Contourner le filtre d'introspection. Certains développeurs bloquent le mot-clé
__schemapar expression régulière. Or GraphQL ignore les espaces, sauts de ligne et virgules là où une regex les détecte : insérer un tel caractère après__schemasuffit parfois à passer. - Changer de méthode. Tenter l'introspection en GET peut contourner un filtrage qui ne vise que les POST :
Contourner les limites de débit avec des alias¶
Les alias permettant de regrouper de nombreuses opérations dans une seule requête HTTP, on peut envoyer des centaines d'appels d'un coup là où une limite de débit ne compte que les requêtes — pas les opérations qu'elles contiennent. C'est dévastateur pour un brute-force d'authentification ou de codes :
mutation {
attempt0: login(input:{password: "motdepasse1", username: "victime"}) { token success }
attempt1: login(input:{password: "motdepasse2", username: "victime"}) { token success }
attempt2: login(input:{password: "motdepasse3", username: "victime"}) { token success }
}
Construire manuellement des centaines d'alias serait fastidieux ; un court script JavaScript, exécuté dans la console du navigateur, génère la requête à partir d'une liste de mots de passe :
copy(`motdepasse1,motdepasse2,motdepasse3,...`.split(',').map((element, index) =>
`attempt$index: login(input:{password: "$password", username: "victime"}) { token success }`
.replaceAll('$index', index).replaceAll('$password', element)).join('\n'));
CSRF sur GraphQL¶
Tant que l'API n'accepte que des POST en application/json, elle résiste bien au CSRF, ce type de requête étant difficile à provoquer depuis le navigateur d'une victime. En revanche, si elle accepte des méthodes alternatives (GET) ou le type x-www-form-urlencoded, elle y devient probablement vulnérable.
En passant en x-www-form-urlencoded, il faut reformater le corps de la requête. La version JSON :
{"query":"mutation changeEmail($input: ChangeEmailInput!) { changeEmail(input: $input) { email } }","operationName":"changeEmail","variables":{"input":{"email":"attaquant@exemple.net"}}}
devient, sous forme encodée :
query=mutation%20changeEmail($input:ChangeEmailInput!){changeEmail(input:$input){email}}&operationName=changeEmail&variables={"input":{"email":"attaquant@exemple.net"}}
Le passage de l'un à l'autre suit quelques règles mécaniques : retirer les guillemets, remplacer les virgules de séparation par &, supprimer les espaces et retours à la ligne inutiles (et encoder en %20 ceux qui sont nécessaires), remplacer les : d'affectation par =. On génère ensuite un formulaire HTML auto-soumis pointant vers l'endpoint, dont le clic — ou le simple chargement — déclenche la mutation au nom de la victime.
Aide-mémoire¶
| Objectif | Approche |
|---|---|
| Détecter un endpoint | query{__typename} sur les chemins courants |
| Tester d'autres vecteurs | Rejouer la query en GET ou x-www-form-urlencoded |
| Accès non autorisé | Demander un identifiant masqué de la liste (IDOR) |
| Cartographier le schéma | Introspection ; sinon suggestions (Clairvoyance), filtre contourné, GET |
| Contourner une limite de débit | Regrouper les opérations via alias |
| CSRF | Méthode alternative ou x-www-form-urlencoded + formulaire auto-soumis |
Les outils Clairvoyance (reconstruction de schéma) et les fonctions d'introspection intégrées à Burp Suite sont les compagnons naturels d'un test d'API GraphQL.