Se défendre contre les attaques GraphQL : analyse approfondie des vulnérabilités courantes
Cet article examine en détail les vulnérabilités GraphQL les plus courantes, les raisons de leur apparition et les moyens de les atténuer.
Se défendre contre les attaques GraphQL : analyse approfondie des vulnérabilités courantes
GraphQL a révolutionné le développement d'API grâce à son approche souple et efficace de l'interrogation des données. Mais comme toute technologie, il pose ses propres défis de sécurité.
Cet article s'appuie sur l'expérience d'Ostorlab dans l'automatisation de la détection et du test des vulnérabilités GraphQL.
Nous examinerons en détail les vulnérabilités GraphQL les plus courantes, les raisons de leur apparition et les moyens de les atténuer.
Qu'est-ce que GraphQL ?
GraphQL est un langage de requête pour les API et un environnement d'exécution de ces requêtes. Il permet aux clients de demander exactement les données dont ils ont besoin, ni plus ni moins, ce qui peut optimiser sensiblement les interactions avec l'API.
Développé à l'origine par Facebook en 2012 puis publié en open source en 2015, GraphQL a été conçu pour dépasser les limites des API REST traditionnelles, comme la récupération excessive ou insuffisante de données.
Selon Wappalyzer, plus de 176 000 sites web utilisent GraphQL à ce jour. Ce nombre croît rapidement à mesure que de nouvelles entreprises adoptent GraphQL, dont AWS, PayPal et GitHub. Consultez le GraphQL Landscape pour la liste complète.

query CurrentUser {
currentUser {
name
age
}
}
Réponse :
{
"currentUser":{
"name":"John Doe",
"age":23
}
}
Principales caractéristiques
- Point d'accès unique : les API GraphQL exposent un seul point d'accès (endpoint) par lequel passent toutes les interactions.
- Récupération précise des données : les clients demandent exactement ce dont ils ont besoin, ce qui réduit la charge sur le réseau.
- Schéma fortement typé : un schéma bien défini spécifie les types et leurs relations, ce qui permet des requêtes puissantes. L'intégrité du schéma et la sûreté de typage intégrées à GraphQL rendent inutile le versionnement des données.

Les types dans GraphQL
Les schémas GraphQL reposent sur trois types principaux :
- Les types racine
- Les types scalaires
- Les types objet

Types racine
1. Queries : récupèrent ou lisent des données depuis le serveur.
2. Mutations : modifient ou manipulent des données (création, mise à jour, suppression).
3. Subscriptions : permettent aux clients de recevoir des mises à jour de données en temps réel.

Types scalaires
Les types scalaires représentent des valeurs simples : entiers, chaînes de caractères, booléens, etc. Ce sont les briques de base d'un schéma.
Types objet
Les types objet représentent des entités complexes comportant plusieurs champs. Ces champs peuvent être des scalaires ou d'autres types objet. Par exemple :
type Human {
id: String
name: String
homePlanet: Planet
}
type Planet {
id: String
name: String
}

Découverte du schéma GraphQL et introspection.
GraphQL propose l'introspection, qui permet aux développeurs d'interroger le schéma pour connaître les types, les requêtes et les mutations disponibles (on peut la comparer à une requête OPTIONS en REST).
Même si l'introspection n'est pas un problème de sécurité en soi et qu'elle est utile pendant le développement, les attaquants peuvent l'exploiter pour mieux comprendre les capacités de l'API et, potentiellement, abuser de votre API GraphQL.

L'introspection est activée
Exemple de requête d'introspection pour obtenir toutes les mutations :
Requête
{
__schema {
mutationType {
kind
name
fields {
name
description
deprecationReason
}
}
}
}
Réponse
{
"__schema":{
"mutationType":{
"kind":"OBJECT",
"name":"Mutation",
"fields":[
{
"name":"createUser",
"description":"Create a new user"
}
]
}
}
}
Exemple d'introspection capable d'extraire l'intégralité du schéma, y compris les queries, les mutations et les objectTypes :
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
fields(includeDeprecated: true) {
name
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
}
}
Contourner les filtres regex sur `__schema`
Lorsque les développeurs désactivent l'introspection, ils peuvent utiliser une regex pour exclure le mot-clé __schema des requêtes. Vous pouvez essayer des caractères comme les espaces, les sauts de ligne et les virgules : GraphQL les ignore, mais pas les filtres regex défaillants.
Par exemple, si le développeur a seulement exclu __schema{, la requête d'introspection ci-dessous, avec un saut de ligne après __schema, ne serait pas exclue :
{
"query": "query{__schema
{queryType{name}}}"
}
Déduire le schéma à partir des suggestions et de la gestion des erreurs.
Parfois, même lorsque l'introspection est désactivée, il reste possible de déduire une partie du schéma GraphQL en exploitant les suggestions et les messages d'erreur renvoyés par le serveur. Par exemple, lorsqu'une requête contient un champ mal orthographié mais proche d'un champ existant, le serveur GraphQL peut renvoyer une erreur qui suggère le nom de champ correct.

Exemple :

Il en va de même pour la déduction des mutations et des arguments.

Désactiver l'introspection
Dans la mesure du possible, l'introspection doit être désactivée dans les environnements de production et activée uniquement pendant le développement. Selon la spécification GraphQL :
"All types and directives defined within a schema must not have a name that begins with __ (two underscores), as this is reserved exclusively for GraphQL's introspection system."
Une approche courante pour désactiver l'introspection consiste à exclure les champs dont le nom commence par __.
Cette méthode limite efficacement l'introspection, mais il est essentiel d'appliquer cette configuration de manière cohérente dans tous les environnements.
Attaques par déni de service (DoS)
Dans cette section, nous allons étudier en détail plusieurs attaques par déni de service (DoS) que des attaquants peuvent (et vont) employer contre votre application.
GraphQL expose les données de l'application sous forme de graphe, ce qui permet aux clients de récupérer des données en parcourant les relations entre les nœuds (les types).
La plupart des vulnérabilités listées ci-dessous découlent de la partie Graph de GraphQL.
Fragments circulaires (sévérité faible)
Les fragments GraphQL permettent de réutiliser la logique des requêtes en définissant des champs réutilisables.
Cependant, les fragments circulaires peuvent servir à fabriquer des requêtes qui consomment des ressources serveur excessives.
Exemple de fragment circulaire :
fragment UserFields on User {
comments {
...CommentFields
}
}
fragment CommentFields on Comment {
owner {
...UserFields
}
}
Comme vous pouvez le voir, le fragment UserFields référence CommentFields, et CommentFields référence à son tour UserFields, ce qui provoque une boucle infinie.

Les serveurs GraphQL identifient et rejettent généralement les requêtes contenant des références cycliques entre fragments, en renvoyant un message d'erreur.
Toutefois, dans certains cas, l'implémentation du serveur ne respecte pas strictement la spécification GraphQL.
Il faut donc prendre des mesures supplémentaires :
Détection des fragments circulaires : utilisez des outils d'analyse de schéma comme GraphQL-ESLint pour détecter et prévenir les fragments circulaires.
Mettre en place une analyse du coût des requêtes : attribuez un coût aux champs et aux requêtes, et rejetez celles qui dépassent une limite prédéfinie. Par exemple, si vous utilisez Apollo server, vous pouvez recourir à la bibliothèque graphql-query-complexity :
const {
queryComplexity,
simpleEstimator
} = require('graphql-query-complexity');
const server = new ApolloServer({
schema,
validationRules: [
queryComplexity({
estimators: [
simpleEstimator({
defaultComplexity: 1
})
],
maximumComplexity: 1000, // Reject queries that exceed this cost
}),
],
});
Surcharge d'alias (sévérité faible)
La surcharge d'alias dans GraphQL se produit lorsqu'un attaquant utilise un très grand nombre d'alias dans une requête pour saturer les capacités de traitement du serveur.
Dans GraphQL, les alias permettent aux clients de demander plusieurs fois le même champ sous des noms différents. Or, un usage excessif d'alias dans une même requête peut conduire à des attaques par déni de service (DoS) en épuisant les ressources du serveur. Cette attaque peut dégrader les performances ou provoquer une interruption complète du service.
Par exemple :
query AliasOverLoading {
alias1: __typename
alias2: __typename
alias3: __typename
alias4: __typename
alias5: __typename
}
Impact de la surcharge d'alias sur la sécurité :
La surcharge d'alias présente des risques importants pour les API GraphQL. L'une des principales conséquences est le déni de service (DoS) : une requête contenant un nombre excessif d'alias oblige le serveur à allouer des ressources disproportionnées pour la traiter et y répondre. Cela peut ralentir, voire faire tomber le service, et nuire à sa disponibilité. Le serveur peut en outre subir un épuisement de ses ressources, car le traitement simultané de nombreux alias peut entraîner une saturation de la mémoire, des pics de CPU ou une forte dégradation des performances.
Plusieurs stratégies permettent d'atténuer le risque de surcharge d'alias. D'abord, imposer des délais d'expiration aux requêtes est une mesure efficace. En fixant un temps d'exécution maximal, le serveur peut interrompre automatiquement les requêtes trop longues à résoudre, ce qui empêche des requêtes malveillantes de surcharger les ressources et de provoquer un DoS.
Une autre mesure importante consiste à limiter le nombre d'alias, comme nous l'avons vu dans la section précédente.
Références circulaires (sévérité moyenne)

Un attaquant peut exploiter cette situation pour fabriquer une requête récursive qui sature les ressources du serveur et provoque un déni de service et un épuisement des ressources.
Lors d'un test de l'impact de cette vulnérabilité (sur une cible réelle) utilisant Django, graphene-python et une instance Cloud SQL dotée de 42 GB de mémoire, de 8 vCPU et de 2,2 TB de stockage SSD,
nous avons réussi à fabriquer une requête récursive qui a fait atteindre à notre instance Cloud SQL 100 % d'utilisation du CPU, et le problème a persisté jusqu'à ce que nous devions tuer manuellement ces transactions SQL bloquées,

Supposons que nous voulions exposer un utilisateur ainsi que la liste des commentaires qu'il a rédigés, avec les types suivants :
type User {
id: ID!
comments: Comment
username: String
}
type Comment {
id: ID
owner: User!
content: String!
}
Le type User introduit une référence circulaire, qui permet de créer des requêtes complexes ou récursives.
Exemple de requête circulaire :
query {
user(id: 1) {
id
comments {
id
owner {
id
comments {
id
owner {
id
... # can go forever
}
}
}
}
}
}

Pour atténuer le risque de références circulaires dans GraphQL, vous pouvez :
Remanier le schéma pour éviter les références circulaires directes lorsque c'est possible. Par exemple, utilisez un nouveau type `LimitedUser` dans le type `Comment` pour briser la boucle :
type User {
id: ID!
username: String!
comments: [Comment]!
}
type LimitedUser {
id: ID!
username: String!
}
type Comment {
id: ID!
owner: LimitedUser!
content: String!
}

Cette solution peut toutefois se révéler inefficace, car elle risque de casser les clients et nécessite des changements majeurs dans la base de code.
Imposer des limites de profondeur aux requêtes : fixez une limite à la profondeur des requêtes pour empêcher les requêtes excessivement profondes.
La plupart des implémentations de serveurs GraphQL proposent cette fonctionnalité par défaut, par exemple dans Apollo Server :
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
schema,
validationRules: [depthLimit(10)], // Set maximum query depth to 10
});
Force brute sur l'authentification par regroupement d'alias (moyenne)
La force brute sur l'authentification par regroupement d'alias dans GraphQL consiste, pour un attaquant, à tirer parti de la fonctionnalité d'alias pour automatiser les tentatives de connexion, ce qui facilite l'envoi de nombreuses combinaisons d'identifiants dans une seule requête.
Dans GraphQL, les alias permettent aux clients d'envoyer plusieurs versions d'une même requête sous des noms différents. Les attaquants en profitent pour regrouper des requêtes de connexion dans une seule requête, avec un nom d'alias différent pour chaque tentative. Cela permet une attaque par force brute efficace, qui contourne les protections classiques de limitation de débit et submerge le système d'authentification de tentatives de connexion.
Exemple :
query loginBatch {
login1: login(username: "user1", password: "password1") {
token
}
login2: login(username: "user2", password: "password2") {
token
}
login3: login(username: "user3", password: "password3") {
token
}
...
}
Pour atténuer ce problème, vous devez limiter le nombre d'alias autorisés dans une même requête. En configurant des restrictions côté serveur ou en utilisant des outils comme GraphQL Armor, vous pouvez plafonner le nombre d'alias par requête
Limiter le nombre de tentatives de connexion échouées par utilisateur peut aussi contribuer efficacement à résoudre le problème.
Mauvaise configuration des autorisations dans GraphQL (sévérité élevée)
Dans les API GraphQL, une vulnérabilité grave peut apparaître lorsque l'accès à des données sensibles est correctement restreint sur un chemin de requête mais reste exposé sur un autre, à cause de contrôles d'accès incohérents. Les attaquants peuvent alors récupérer des données non autorisées en empruntant un autre chemin de requête qui contourne les restrictions.
Ce type de vulnérabilité est facile à introduire.
Voici un exemple simple (avec Django et django-graphene) :
Nous avons une petite plateforme de réseau social où les utilisateurs peuvent discuter entre eux et aussi publier des posts.
from django.db import models
from django.contrib.auth.models import AbstractUser
class SocialUser(AbstractUser):
pass
class Post(models.Model):
user = models.ForeignKey(SocialUser, related_name='posts', on_delete=models.CASCADE)
content = models.TextField()
class Discussion(models.Model):
user = models.ForeignKey(SocialUser, related_name='discussions', on_delete=models.CASCADE)
content = models.TextField()
Et avec `graphene_django`, nous avons les types suivants :
class DiscussionType(DjangoObjectType):
class Meta:
model = models.Discussion
class PostType(DjangoObjectType):
class Meta:
model = models.Post
class UserProfileType(DjangoObjectType):
class Meta:
model = models.SocialUser
Nous déclarons les requêtes suivantes :
class Query(graphene.ObjectType):
my_discussions = graphene.List(DiscussionType)
posts = graphene.List(PostType)
my_profile = graphene.Field(UserProfileType)
def resolve_my_discussions(self, info):
user = info.context.user
if user.is_anonymous:
raise Exception("Not logged in!")
return user.discussions.all() # Only the discussions of the logged in user
def resolve_posts(self, info):
user = info.context.user
if user.is_anonymous:
raise Exception("Not logged in!")
return models.Post.objects.all() # All posts are public of course.
def resolve_my_profile(self, info):
user = info.context.user
if user.is_anonymous:
raise Exception("Not logged in!")
return user
À première vue, rien d'anormal. Un utilisateur ne peut voir que ses propres discussions

Vous pouvez aussi récupérer la liste des posts publics des autres utilisateurs.

Mais si vous regardez de plus près le type `PostType`, vous constatez qu'il possède un champ UserProfileType.

C'est parce que le modèle Post possède bien une `ForeignKey`vers le modèle SocialUser et que django-graphene l'a exposé en utilisant le premier Object Type qu'il a pu trouver.

Nous pouvons donc exécuter la requête suivante pour obtenir un accès non autorisé aux discussions d'autres utilisateurs.

Extensions GraphQL : le mode debug en exemple.
Que sont les extensions GraphQL ?
Les extensions GraphQL sont des morceaux de code qui ajoutent de nouvelles fonctionnalités à votre configuration GraphQL. Elles permettent de faire des choses que GraphQL ne fait pas habituellement par lui-même, comme ajouter de nouveaux ObjectTypes et exposer des informations de débogage.
Bien que ce soit une fonctionnalité utile, certaines de ces extensions, qui sont implémentées par défaut dans certains serveurs GraphQL comme Graphene-Django et graphql-ruby, peuvent causer de sérieux problèmes de sécurité.
Debug GraphQL (sévérité élevée)
Pour traiter les problèmes dans GraphQL, les développeurs utilisent des applications d'informations de débogage.
Lorsque le mode debug est activé, un serveur GraphQL fournit, en réponse aux requêtes des clients, des messages détaillés sur les erreurs du serveur backend, qui ne sont normalement pas affichés.
Par exemple, au lieu de renvoyer des messages d'erreur standard, un client peut recevoir une stack trace et des informations détaillées sur l'erreur.
Le mode debug de GraphQL est implémenté par défaut dans de nombreuses implémentations GraphQL, mais pas dans toutes (voir le tableau ci-dessous).
Précieux pendant le développement, il peut, s'il reste activé en production, exposer des informations sensibles sur la structure interne du serveur et sur les détails de son implémentation.
Lorsque le mode debug est activé, les réponses d'erreur peuvent inclure :
- Des stack traces détaillées
- Des informations sur les requêtes de base de données (y compris les requêtes SQL)
- Des chemins et des noms de fichiers internes au serveur
- Des détails de configuration sensibles
Exemple de réponse avec le mode debug activé dans Django Graphene, qui s'appuie sur le debug de Django :
{
query {
nonexistentField {
id
}
}
_debug {
sql {
sql
transId
transStatus
isoLevel
encoding
}
}
}
{
"errors":[
{
"message":"Cannot query field \"nonexistentField\" on type \"Query\".",
"locations":[
{
"line":3,
"column":3
}
],
"path":[
"nonexistentField"
]
}
],
"data":null,
"extensions":{
"debug":{
"sql":[
{
"sql":"SELECT \"auth_user\".\"id\", \"auth_user\".\"password\", \"auth_user\".\"last_login\", \"auth_user\".\"is_superuser\", \"auth_user\".\"username\", \"auth_user\".\"first_name\", \"auth_user\".\"last_name\", \"auth_user\".\"email\", \"auth_user\".\"is_staff\", \"auth_user\".\"is_active\", \"auth_user\".\"date_joined\" FROM \"auth_user\" WHERE \"auth_user\".\"id\" = %s LIMIT 21",
"time":"0.000",
"params":[
"1"
]
}
],
"python_version":"3.8.5",
"django_version":"3.1.3",
"graphene_version":"2.1.8",
"graphql_version":"3.0.0"
}
}
}
Impact du mode debug de GraphQL sur la sécurité :
-
Divulgation d'informations : les informations de débogage peuvent révéler des détails internes sur la structure du serveur GraphQL, ses dépendances et ses vulnérabilités potentielles, que des attaquants peuvent exploiter pour préparer des attaques plus ciblées.
-
Exposition de données sensibles : les stack traces, les messages d'erreur et surtout les requêtes SQL peuvent contenir par inadvertance des informations sensibles comme la structure de la base de données, des chemins de fichiers internes ou des variables d'environnement.
-
Exploitation facilitée : les messages d'erreur détaillés et les requêtes SQL peuvent aider les attaquants à affiner leurs attaques, en leur indiquant immédiatement ce qui a fonctionné ou non dans leurs requêtes malveillantes.
-
Fuite d'informations de performance : les informations de durée fournies pour les requêtes SQL peuvent être utilisées par les attaquants pour déduire la structure de la base de données ou pour mener des attaques par analyse temporelle.
Pour atténuer les risques liés au mode debug de GraphQL, il faut avant tout.
Il est essentiel de désactiver le mode debug en production. Par exemple, si vous utilisez Django avec Graphene-Django, le mode debug de GraphQL est contrôlé par les mêmes paramètres que le debug de Django.
settings.py
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
Conclusion
Si GraphQL offre des avantages considérables pour le développement d'API, comme des requêtes flexibles et une récupération efficace des données, il introduit aussi des défis de sécurité qui lui sont propres. Pour les relever, il faut comprendre en profondeur non seulement les vulnérabilités potentielles, mais aussi le serveur GraphQL que vous utilisez et les fonctionnalités de sécurité qu'il prend en charge.
Voici une liste des serveurs GraphQL les plus populaires et des fonctionnalités qu'ils prennent en charge :
✅ - Activé par défaut
⚠️ - Désactivé par défaut
❌ - Non pris en charge

Source : graphql-threat-matrix