Retour au cours

cyber / cybersecurite-fondamentale

Sécurité des API GraphQL

Leçon 271 exercice

Explication

Ce que vous allez apprendre

  • Comprendre pourquoi l'introspection GraphQL doit être désactivée ou restreinte en production
  • Limiter la profondeur ET la complexité des requêtes pour éviter un déni de service applicatif
  • Reconnaître une attaque par batching via alias qui contourne un rate limiting classique
  • Vérifier l'autorisation au niveau de chaque champ résolu, pas seulement à la racine de la requête
  • Adapter les principes de sécurité API déjà vus (leçon 17) aux spécificités du paradigme GraphQL

Dans quel contexte ?

Une API GraphQL expose une requête permettant de lister des utilisateurs avec leurs amis. Un attaquant envoie une requête imbriquant "amis des amis des amis" sur six niveaux de profondeur : ce qui ressemble à une seule requête HTTP anodine déclenche en réalité des milliers de résolutions côté serveur, saturant la base de données. Un rate limiting classique basé sur le nombre de requêtes HTTP par minute ne voit rien passer d'anormal, car une seule requête a bien été envoyée.

Pourquoi GraphQL demande une réflexion de sécurité différente

Les leçons précédentes sur la sécurité des API (leçon 17) portaient implicitement sur des API REST classiques, où chaque endpoint expose une action précise et prévisible. GraphQL change ce modèle : un client peut construire des requêtes flexibles et imbriquées, ce qui offre une grande puissance côté développement mais ouvre aussi des angles d'attaque spécifiques à ce paradigme.

L'introspection, une fonctionnalité pratique devenue un risque

GraphQL permet, par défaut, à n'importe quel client de demander au serveur "montre-moi ton schéma complet" — utile en développement pour l'auto-complétion des outils, mais cela revient à distribuer publiquement le plan détaillé de toute l'API, y compris des champs ou types que l'on n'avait pas forcément l'intention d'exposer aussi ouvertement. En production, cette fonctionnalité doit généralement être désactivée ou strictement restreinte.

Le vrai piège : une seule requête HTTP peut coûter cher au serveur

Piège fréquent

Avec REST, une requête HTTP correspond généralement à un coût prévisible côté serveur. Avec GraphQL, un client peut imbriquer des requêtes en profondeur et démultiplier le travail réel effectué côté serveur en une seule requête apparemment innocente. Limiter uniquement le NOMBRE de requêtes HTTP par minute ne protège pas contre ce type d'abus : il faut limiter la profondeur ET la complexité de chaque requête individuelle.

Le batching par alias, une variante du même problème

AttaqueMécanismePourquoi le rate limiting classique échoue
Requête profondeImbrication de champs sur plusieurs niveauxUne seule requête HTTP, coût caché en profondeur
Batching par aliasPlusieurs opérations distinctes dans une requêteUne seule requête HTTP, des centaines d'opérations métier

GraphQL permet d'envoyer plusieurs opérations différentes (via des alias) dans une seule requête HTTP. Un attaquant peut ainsi transformer une seule requête réseau en des centaines d'opérations métier distinctes, contournant totalement un rate-limiting basé sur le nombre de requêtes plutôt que sur leur coût réel.

L'autorisation champ par champ, pas seulement à la racine

Vérifier les droits uniquement au niveau de la requête globale ne suffit pas : si un champ précis expose une donnée sensible (comme l'email d'un autre utilisateur imbriqué dans un résultat), l'autorisation doit être vérifiée au niveau de CE champ, à chaque fois qu'il est résolu.

Commandes & code

Sécurité des API GraphQL

GraphQL déplace la surface d'attaque : un schéma unique, des requêtes arbitrairement imbriquées, et souvent aucune limite de profondeur par défaut.

graphql
# Requête d'introspection : premier réflexe d'un attaquant contre une API GraphQL mal configurée
# Elle révèle TOUT le schéma (types, champs, mutations, arguments) sans authentification
query IntrospectionAttack {
  __schema {
    types {
      name
      fields {
        name
        args { name type { name } }
      }
    }
  }
}
# Contre-mesure : désactiver l'introspection en production (selon la lib GraphQL utilisée)
python
# Attaque par profondeur/complexité : une requête imbriquée peut exploser le coût CPU/DB (DoS applicatif)
query DeepAttack {
  user(id: 1) {
    friends {          # niveau 1
      friends {         # niveau 2
        friends {        # niveau 3 - et ainsi de suite, chaque niveau multiplie les résolutions
          friends { friends { friends { id } } }
        }
      }
    }
  }
}

# Défense en Python avec strawberry-graphql : limiter profondeur ET complexité, pas seulement l'une des deux
import strawberry
from strawberry.extensions import QueryDepthLimiter
from strawberry.extensions.field_extension import FieldExtension

schema = strawberry.Schema(
    query=Query,
    extensions=[
        QueryDepthLimiter(max_depth=6),          # rejette toute requête dépassant 6 niveaux d'imbrication
    ],
)
# Complexité : pondérer chaque champ (ex: liste = coût x taille max) et rejeter au-delà d'un budget total
python
# Batching attack : contourner un rate-limit HTTP en empilant N requêtes dans UNE seule requête GraphQL (alias)
query BruteforceViaAlias {
  attempt1: login(username: "admin", password: "pass1") { token }
  attempt2: login(username: "admin", password: "pass2") { token }
  attempt3: login(username: "admin", password: "pass3") { token }
  # ... répété 1000 fois -> 1000 tentatives dans une seule requête HTTP, invisible d'un rate-limiter naïf
}

# Défense : limiter le nombre d'alias par requête ET rate-limiter par coût de requête, pas par nombre de requêtes HTTP
def reject_too_many_aliases(document, max_aliases=20):
    alias_count = sum(1 for node in document if getattr(node, "alias", None))
    if alias_count > max_aliases:
        raise Exception("Trop d'alias dans une seule requête GraphQL")
python
# Autorisation au niveau champ (field-level), pas seulement au niveau requête
# Erreur classique : vérifier les droits sur la query "user" mais pas sur le champ imbriqué "user.salary"
import strawberry
from typing import Optional

def require_role(role: str):
    def decorator(resolver):
        def wrapped(root, info, **kwargs):
            current_user = info.context["request"].state.user
            if role not in current_user.roles:
                raise PermissionError(f"Rôle '{role}' requis pour ce champ")
            return resolver(root, info, **kwargs)
        return wrapped
    return decorator

@strawberry.type
class User:
    id: int
    name: str

    @strawberry.field
    @require_role("hr_admin")   # protège CE champ précis, indépendamment du reste de la requête
    def salary(self) -> Optional[float]:
        return self._salary

Résumé

  • L'introspection expose le schéma complet : à désactiver ou restreindre en production.
  • Limiter la profondeur ET la complexité des requêtes, pas juste le nombre de requêtes HTTP (les alias contournent le rate-limit classique).
  • L'autorisation doit être vérifiée au niveau de chaque champ résolu, pas seulement à la racine de la requête.
  • Le batching par alias transforme une seule requête HTTP en des centaines d'opérations métier : le rate-limiting doit être basé sur le coût, pas le nombre de requêtes.

Exercices pratiques

1 disponible
1

Mission : une seule requête, mille tentatives de connexion

Objectif : Identifier une attaque de batching par alias dans une requête GraphQL et concevoir une défense qui ne se limite pas au comptage de requêtes HTTP.

Contexte

L'équipe sécurité de Technologik observe dans les logs applicatifs GraphQL une seule requête HTTP contenant une mutation login répétée 500 fois sous des alias différents (attempt1, attempt2, ... attempt500), chacune avec un mot de passe différent pour le même nom d'utilisateur admin. Le rate-limiter en place autorise 60 requêtes HTTP par minute et n'a rien bloqué.

Résoudre l’exercice →