Neutron, notre moteur d’IA, a obtenu un score de 96,75 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Sécurité

Sécurité

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.

Panorama GraphQL
Panorama GraphQL
Exemple de requête GraphQL :

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.

Caractéristiques de GraphQL
Principales caractéristiques de GraphQL

Les types dans GraphQL

Les schémas GraphQL reposent sur trois types principaux :

- Les types racine
- Les types scalaires
- Les types objet

Les types dans GraphQL
Les types dans GraphQL

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.

Les types dans GraphQL 2
Les types dans GraphQL

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
}

Types objet
Graphe de types

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.

Schéma GraphQL et introspection
Introspection du schéma 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.

Schéma GraphQL et introspection
Déduire le schéma à partir des suggestions et de la gestion des erreurs

Exemple :

exemple de suggestions de schéma
Exemple de suggestions de schéma GraphQL

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

exemple de suggestions de mutation
Exemple de suggestions de schéma GraphQL pour les 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.

Exemple de fragment circulaire
Exemple de fragment circulaire

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)

Références circulaires
Références circulaires
Les références circulaires apparaissent lorsque des types objet GraphQL se référencent mutuellement, ce qui peut donner lieu à des requêtes profondément imbriquées.

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,

Instance Cloud SQL avec GraphQL
Requête récursive qui a fait atteindre à notre **instance Cloud SQL** 100 % d'utilisation du CPU

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 
          }
        }
      }
    }
  }
}

référence circulaire

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!
}

Référence circulaire corrigée

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

requête discussions

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

requête posts

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

PostType

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.

code des types

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

accès non autorisé à 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

matrice de menaces GraphQL

Source : graphql-threat-matrix