Prendre RDVContact
Retour à toutes les notes
1 juin 2026

Failles BOLA dans les API GraphQL : La menace silencieuse

queryUserAccountProfile!BOLA

Failles BOLA dans les API GraphQL : La menace silencieuse

Le défaut d'autorisation au niveau de l'objet ou Broken Object Level Authorization (BOLA) reste la faille la plus critique affectant les applications web modernes. Lorsque nous migrons vers des architectures orientées graphe, la surface d'attaque ne fait pas que changer : elle s'étend de manière imprévisible. Cet article explore les vulnérabilités BOLA dans les API GraphQL, en analysant précisément pourquoi les mentalités d'autorisation traditionnelles centrées sur REST échouent lamentablement lorsqu'elles sont appliquées aux graphes. Nous décortiquerons le problème, analyserons pourquoi il est difficile à résoudre, proposerons une architecture résiliente, fournirons une implémentation concrète et aborderons les pièges que vous rencontrerez inévitablement.

Si vous traitez GraphQL comme un simple point d'accès REST amélioré, votre API est déjà vulnérable. Il est temps de sécuriser votre couche d'autorisation.

Le problème : L'autorisation au niveau de l'objet est défaillante

Dans une architecture REST traditionnelle, l'autorisation est relativement simple car la topologie des points d'accès correspond clairement à la topologie des données. Vous demandez GET /api/users/123/financials. Le routeur intercepte la requête, exécute un middleware qui vérifie si l'utilisateur authentifié a l'autorisation d'accéder aux données financières de l'utilisateur 123, puis autorise ou rejette la demande. Les limites sont fixes, bien définies et faciles à inspecter.

GraphQL détruit cette simplicité.

Avec GraphQL, le client dicte la forme de la réponse. Le point d'accès est toujours /graphql, et le corps de la requête contient une requête qui peut parcourir plusieurs entités, relations et niveaux de profondeur. Une seule requête peut accéder au profil d'un utilisateur, à ses amis, aux publications de ses amis et aux commentaires sur ces publications.

query BOLAExploit {
  user(id: "current-user-id") {
    name
    organization {
      id
      members(first: 100) {
        id
        email
        salary # Champ vulnérable
      }
    }
  }
}

Dans l'exemple ci-dessus, l'attaquant commence sur un objet qu'il contrôle (user), navigue vers un objet partagé (organization), puis demande un champ sensible (salary) sur tous les membres de cette organisation. Si la logique d'autorisation vérifie uniquement si l'utilisateur appartient à l'organisation mais omet de vérifier si l'utilisateur est autorisé à lire le champ salary des autres membres, vous faites face à une faille BOLA.

Le problème fondamental est que les résolveurs GraphQL s'exécutent de manière indépendante. Le résolveur de salary sait qu'il doit renvoyer un nombre décimal pour un utilisateur donné, mais il manque souvent d'informations sur qui interroge et comment il est arrivé à ce nœud. Cette perte de contexte est le terreau des failles BOLA.

Pourquoi les vulnérabilités BOLA dans les API GraphQL sont difficiles à détecter

Les vulnérabilités BOLA dans les API GraphQL sont notoirement difficiles à identifier par des analyses automatisées ou des revues de code informelles. En voici les raisons :

1. Profondeur et navigation dans le graphe

Les attaquants ne se contentent pas d'interroger les nœuds racines ; ils parcourent le graphe pour contourner les contrôles de niveau supérieur. Une requête racine pour users(id: "123") est peut-être protégée, mais qu'en est-il de post(id: "456") { author { id email } } ? Si le résolveur de author n'effectue pas exactement les mêmes contrôles d'autorisation que la requête racine user, les données sont exposées. Le graphe est interconnecté, ce qui signifie qu'il existe des dizaines de chemins d'accès pour un même nœud de données. Sécuriser un seul chemin ne sert à rien si les autres restent ouverts.

2. Fragmentation du contexte

Dans de nombreux frameworks (comme Apollo Server ou Yoga), les résolveurs sont des fonctions pures qui reçoivent un parent, des arguments args, un contexte context et des informations info. Les développeurs placent souvent la logique d'autorisation dans la configuration du contexte (par exemple, en extrayant l'ID de l'utilisateur à partir d'un JWT), mais oublient de transmettre le contexte métier nécessaire aux résolveurs de champs spécifiques. Lorsque l'exécution atteint un champ profondément imbriqué, le résolveur ne dispose plus que de l'objet parent et de l'ID de l'utilisateur. Il lui manque les règles métier complexes indispensables pour prendre une décision d'autorisation correcte.

3. Le comportement par défaut « Fail-Open »

Les frameworks GraphQL exécutent par défaut n'importe quel résolveur si le schéma le permet. À moins d'écrire explicitement du code pour refuser l'accès, la requête est autorisée. Ce paradigme « ouvert par défaut » (fail-open) est intrinsèquement dangereux. Les systèmes sécurisés doivent être fermés par défaut (fail-closed). Si une politique d'autorisation est manquante, la requête doit être rejetée.

4. La sur-extraction (Over-fetching) et le masquage

En REST, un attaquant sollicite un point d'accès spécifique, ce qui rend son intention évidente dans les logs. En GraphQL, un attaquant peut dissimuler un exploit BOLA au sein d'une requête légitime qui récupère des dizaines de champs sécurisés. La charge utile malveillante est masquée par le bruit, rendant les systèmes de détection d'intrusion (IDS) et les pare-feu applicatifs web (WAF) complètement aveugles face à l'attaque.

Solutions architecturales pour l'autorisation GraphQL

Pour éliminer les failles BOLA, nous devons abandonner les contrôles d'autorisation ad hoc dispersés dans nos résolveurs. Nous avons besoin d'une architecture d'autorisation centralisée, déterministe et fermée par défaut (fail-closed).

L'approche par moteur de politiques (Policy Engine)

L'architecture la plus résiliente découple entièrement la logique d'autorisation des résolveurs GraphQL. Les résolveurs ne doivent pas savoir comment évaluer les autorisations ; ils doivent seulement savoir à qui s'adresser. Cela implique l'introduction d'un moteur de politiques (Policy Engine) dédié.

Le moteur de politiques sert de source unique de vérité pour toutes les décisions d'autorisation. Il prend trois entrées :

  1. L'Acteur : L'utilisateur authentifié (par exemple, son ID, ses rôles, ses attributs).
  2. L'Action : L'opération effectuée (par exemple, read, update, delete).
  3. La Ressource : L'objet spécifique accédé (par exemple, l'ID du document, l'ID de l'organisation).

Lorsqu'un résolveur doit renvoyer des données, il interroge le moteur de politiques : « L'Acteur X peut-il effectuer l'Action Y sur la Ressource Z ? »

Contrôle d'accès basé sur les attributs (ABAC)

Le contrôle d'accès basé sur les rôles (RBAC) ne suffit pas à prévenir les failles BOLA. BOLA concerne avant tout l'accès au niveau des objets. Savoir qu'un utilisateur est « Admin » ne sert à rien s'il tente d'accéder à un document appartenant à un autre client (multi-tenant).

Nous devons utiliser le contrôle d'accès basé sur les attributs (ABAC). L'ABAC évalue les politiques en fonction des attributs de l'acteur, de la ressource et de l'environnement.

Par exemple, une politique peut stipuler : « Un utilisateur peut lire un document SI l'attribut organizationId de l'utilisateur correspond à celui du document ET si le document est marqué comme published. »

Directives de schéma

Pour appliquer proprement ces politiques, nous pouvons utiliser les directives de schéma GraphQL. Les directives nous permettent d'attacher des métadonnées déclaratives à notre schéma. Nous pouvons définir une directive @auth qui spécifie les autorisations requises pour un champ ou un objet.

directive @auth(action: String!) on FIELD_DEFINITION | OBJECT

type User @auth(action: "read:user") {
  id: ID!
  email: String! @auth(action: "read:user_email")
  salary: Float @auth(action: "read:user_salary")
}

Cette approche déclarative garantit que les règles d'autorisation sont visibles, documentées et appliquées de manière cohérente avant même l'exécution du résolveur.

Implémentation : Écrire des résolveurs sécurisés

Construisons une implémentation sécurisée en utilisant Node.js, Apollo Server v4 et un moteur de politiques ABAC personnalisé en TypeScript.

Étape 1 : Le moteur de politiques

Tout d'abord, nous définissons un moteur de politiques simple qui évalue les règles basées sur l'utilisateur et la ressource.

// policyEngine.ts
type User = { id: string; role: string; organizationId: string };
type Resource = { __typename: string; id: string; organizationId?: string; ownerId?: string };

export class PolicyEngine {
  can(user: User, action: string, resource: Resource): boolean {
    if (user.role === 'SUPERADMIN') return true;

    switch (resource.__typename) {
      case 'Document':
        return this.checkDocumentAccess(user, action, resource);
      case 'User':
        return this.checkUserAccess(user, action, resource);
      default:
        return false; // Fermé par défaut (Fail-closed)
    }
  }

  private checkDocumentAccess(user: User, action: string, doc: Resource): boolean {
    if (action === 'read') {
      return user.organizationId === doc.organizationId;
    }
    if (action === 'update' || action === 'delete') {
      return user.id === doc.ownerId;
    }
    return false;
  }

  private checkUserAccess(user: User, action: string, targetUser: Resource): boolean {
    if (action === 'read:salary') {
      // Les utilisateurs ne peuvent lire que leur propre salaire, sauf s'ils appartiennent aux RH
      return user.id === targetUser.id || user.role === 'HR';
    }
    if (action === 'read') {
      return user.organizationId === targetUser.organizationId;
    }
    return false;
  }
}

Étape 2 : Initialisation du contexte

Nous instancions le moteur de politiques et l'injectons dans le contexte GraphQL aux côtés de l'utilisateur authentifié.

// context.ts
import { PolicyEngine } from './policyEngine';

export interface GraphQLContext {
  user: User;
  policyEngine: PolicyEngine;
}

export const context = async ({ req }): Promise<GraphQLContext> => {
  const user = authenticateRequest(req); // ex. vérification du JWT
  return {
    user,
    policyEngine: new PolicyEngine(),
  };
};

Étape 3 : Résolveurs sécurisés

À présent, au sein de nos résolveurs, nous utilisons le moteur de politiques pour appliquer le contrôle d'autorisation au niveau des objets avant de renvoyer les données.

// resolvers.ts
export const resolvers = {
  Query: {
    document: async (_, { id }, context: GraphQLContext) => {
      const doc = await database.getDocument(id);
      if (!doc) return null;

      // Contrôle d'autorisation au niveau de l'objet
      if (!context.policyEngine.can(context.user, 'read', { __typename: 'Document', ...doc })) {
        throw new Error('Unauthorized');
      }

      return doc;
    },
  },
  User: {
    salary: (parent, _, context: GraphQLContext) => {
      // parent est l'objet User en cours de résolution
      if (!context.policyEngine.can(context.user, 'read:salary', { __typename: 'User', ...parent })) {
         throw new Error('Unauthorized to view salary');
      }
      return parent.salary;
    }
  }
};

En déportant la logique d'autorisation vers un moteur de politiques dédié et en vérifiant explicitement l'accès après avoir récupéré l'objet mais avant de le renvoyer, nous éliminons la faille BOLA.

Pièges courants et cas particuliers

Même avec une architecture solide, il existe des pièges que vous devez éviter.

1. Le problème d'autorisation N+1

Si vous interrogez une liste de 100 documents et que votre résolveur vérifie l'autorisation pour chaque document individuellement, vous risquez de déclencher 100 requêtes de base de données uniquement pour récupérer les attributs requis par le moteur de politiques. C'est le problème N+1 appliqué à l'autorisation.

Solution : Utilisez DataLoaders pour regrouper la récupération des métadonnées d'autorisation. Mieux encore, poussez les filtres d'autorisation jusqu'à la couche de base de données. Au lieu de récupérer 100 documents pour les filtrer ensuite dans Node.js, modifiez la requête SQL pour ne retourner que les documents que l'utilisateur est autorisé à voir : SELECT * FROM documents WHERE organization_id = ?.

2. Fuite de données par les messages d'erreur

Lorsqu'un utilisateur tente d'accéder à une ressource sans y être autorisé, le fait de renvoyer une erreur telle que Non autorisé à accéder au document 123 confirme que le document 123 existe bel et bien. C'est une fuite d'informations.

Solution : Si un attaquant tente d'accéder à un objet pour lequel il n'a aucune visibilité, l'API doit renvoyer une erreur générique Introuvable ou null, exactement comme si l'objet n'existait pas. Ne renvoyez Non autorisé que si l'utilisateur sait que l'objet existe mais manque d'autorisations spécifiques pour y effectuer l'action.

3. Omission des mutations

Les développeurs ont tendance à sécuriser fortement leurs requêtes (Queries) mais négligent souvent les mutations. Une faille BOLA dans une mutation updateUser est sans doute plus dangereuse que dans une requête user.

Solution : Les mutations exigent les mêmes vérifications ABAC, sinon plus strictes. Vérifiez que l'utilisateur est autorisé à modifier l'objet spécifique avant d'exécuter l'écriture en base de données.

4. Hypothèses sur l'état côté client

Ne faites jamais confiance au client pour fournir le contexte d'autorisation. Si votre mutation accepte input: { id: "123", organizationId: "456", newData: "..." }, vous ne pouvez pas supposer que l'utilisateur appartient réellement à l'organisation organizationId "456". Vous devez récupérer l'objet depuis la base de données et vérifier son état réel par rapport au token authentifié de l'utilisateur.

Le résultat final : Une couche GraphQL endurcie

La correction des vulnérabilités BOLA dans les API GraphQL n'est pas une fonctionnalité que l'on peut ajouter à la va-vite en fin de sprint. Elle exige un changement fondamental dans la façon dont vous concevez votre application. En abandonnant les vérifications ad hoc dans les résolveurs au profit d'un moteur de politiques centralisé, en adoptant l'ABAC et en appliquant un paradigme fermé par défaut, vous transformez votre API GraphQL, passant d'une surface d'attaque massive à une interface robuste et prévisible.

Arrêtez de vous reposer sur la sécurité par l'obscurité. Cessez de faire confiance au client. Considérez chaque requête comme hostile, évaluez chaque exécution de nœud par rapport à des politiques strictes et concevez une API capable de se défendre elle-même. Le coût d'implémentation d'une autorisation robuste au niveau des objets est élevé, mais le coût d'une faille BOLA est catastrophique. Écrivez des résolveurs sécurisés.

Service Seven Labs

Tests de Pénétration VAPT & Cybersécurité

Nous testons les failles de sécurité. Voir nos services de sécurité →
Loading...

Lire la suite

How VAPT Audits Prevent Enterprise Disaster

Discover how VAPT audits prevent enterprise disaster by exposing critical vulnerabilities before the...

Lire l'article

Moving Beyond Chat: The Architecture of Multi-Agent Systems

Single-prompt LLM applications are fragile. Discover how multi-agent orchestration frameworks are br...

Lire l'article
Chat with us