Afspraak makenContact
Terug naar alle notities
1 juni 2026

BOLA-Kwetsbaarheden in GraphQL-API's: De Stille Bedreiging

queryUserAccountProfile!BOLA

BOLA-Kwetsbaarheden in GraphQL-API's: De Stille Bedreiging

Broken Object Level Authorization (BOLA) blijft de meest kritieke kwetsbaarheid die moderne webapplicaties treft. Wanneer we migreren naar op grafen gebaseerde architecturen (graph-based architectures), verandert het aanvalsoppervlak niet alleen; het breidt zich op onvoorspelbare wijze uit. Deze post verkent BOLA-kwetsbaarheden in GraphQL-API's, en ontleedt precies waarom de traditionele, op REST gerichte autorisatiementaliteit catastrofaal faalt wanneer deze wordt toegepast op grafen. We zullen het probleem analyseren, uitleggen waarom het moeilijk op te lossen is, een veerkrachtige architectuur voorstellen, een concrete implementatie bieden en de valkuilen bespreken die je onvermijdelijk zult tegenkomen.

Als je GraphQL behandelt als een chique REST-endpoint, is je API al kwetsbaar. Het is tijd om je autorisatielaag te repareren.

Het Probleem: Autorisatie op Objectniveau is Defect

In een traditionele REST-architectuur is autorisatie relatief eenvoudig omdat de endpoint-topologie clean aansluit op de datatopologie. Je vraagt GET /api/users/123/financials. De router onderschept het verzoek, voert middleware uit die controleert of de geauthenticeerde gebruiker toestemming heeft om toegang te krijgen tot de financiële gegevens van gebruiker 123, en staat het verzoek toe of weigert het. De grenzen zijn vast, goed gedefinieerd en gemakkelijk te controleren.

GraphQL vernietigt deze eenvoud.

Met GraphQL bepaalt de client de vorm van de respons. Het endpoint is altijd /graphql en de request body bevat een query die door meerdere entiteiten, relaties en diepten kan navigeren. Een enkele query kan toegang krijgen tot het profiel van een gebruiker, hun vrienden, de posts van die vrienden en de reacties op die posts.

query BOLAExploit {
  user(id: "current-user-id") {
    name
    organization {
      id
      members(first: 100) {
        id
        email
        salary # Kwetsbaar veld
      }
    }
  }
}

In het bovenstaande voorbeeld begint de aanvaller bij een object dat hij controleert (user), navigeert naar een gedeeld object (organization) en vraagt vervolgens om een gevoelig veld (salary) van alle leden van die organisatie. Als de autorisatielogica alleen controleert of de gebruiker tot de organisatie behoort, maar nalaat om te controleren of de gebruiker het salary-veld van andere leden mag lezen, heb je een BOLA-kwetsbaarheid.

Het kernprobleem is dat GraphQL resolvers onafhankelijk van elkaar worden uitgevoerd. De salary-resolver weet dat hij een float moet retourneren voor een specifieke gebruiker, maar mist vaak de context van wie het vraagt en hoe deze bij deze node is gekomen. Dit verlies van context is de broedplaats voor BOLA.

Waarom BOLA-kwetsbaarheden in GraphQL-API's Moeilijk te Vangen Zijn

BOLA-kwetsbaarheden in GraphQL-API's zijn notoir moeilijk te identificeren via geautomatiseerde scans of oppervlakkige code-reviews. Dit is waarom:

1. Diepte en Grafen-navigatie

Aanvallers bevragen niet alleen de root-nodes; ze navigeren door de graaf om controles op root-niveau te omzeilen. Een root-query voor users(id: "123") kan beveiligd zijn, maar hoe zit het met post(id: "456") { author { id email } }? Als de author-resolver niet exact dezelfde autorisatiecontroles uitvoert als de root user-query, liggen de gegevens op straat. De graaf is onderling verbonden, wat betekent dat er tientallen paden naar dezelfde datanode leiden. Het beveiligen van één pad is nutteloos als de andere open blijven.

2. Context-fragmentatie

In veel frameworks (zoals Apollo Server of Yoga) zijn resolvers pure functies die een parent, args, context en info ontvangen. Ontwikkelaars plaatsen autorisatielogica vaak in de context-configuratie (bijv. het extraheren van het user ID uit een JWT), maar laten na om voldoende zakelijke context door te geven aan de resolvers op veldniveau. Tegen de tijd dat de uitvoering een diep geneste veld bereikt, heeft de resolver alleen het parent-object en het user ID. Hij mist de complexe bedrijfsregels die nodig zijn om een nauwkeurige autorisatiebeslissing te nemen.

3. De "Fail-Open" Standaard

GraphQL-frameworks zullen standaard elke resolver uitvoeren als het schema dit toestaat. Tenzij je expliciet code schrijft om toegang te weigeren, wordt het verzoek toegestaan. Dit "fail-open"-paradigma is inherent gevaarlijk. Veilige systemen moeten fail-closed zijn. Als een autorisatiebeleid ontbreekt, moet het verzoek worden geweigerd.

4. Over-fetching en Maskering

In REST vraagt een aanvaller een specifiek endpoint aan, waardoor hun bedoeling duidelijk is in de logs. In GraphQL kan een aanvaller een BOLA-exploit diep begraven in een legitiem ogende query die tientallen veilige velden ophaalt. De kwaadaardige payload wordt gemaskeerd door de ruis, waardoor intrusion detection-systemen (IDS) en web application firewalls (WAF) volledig blind zijn voor de aanval.

Architectonische Oplossingen voor GraphQL-autorisatie

Om BOLA te elimineren, moeten we afstappen van ad-hoc autorisatiecontroles die verspreid zijn over onze resolvers. We hebben een gecentraliseerde, deterministische en fail-closed autorisatiearchitectuur nodig.

De Policy Engine-aanpak

De meest veerkrachtige architectuur ontkoppelt de autorisatielogica volledig van GraphQL-resolvers. Resolvers moeten niet weten hoe ze machtigingen moeten evalueren; ze moeten alleen weten aan wie ze het moeten vragen. Dit betekent dat er een specifieke Policy Engine wordt geïntroduceerd.

De Policy Engine fungeert als de single source of truth voor alle autorisatiebeslissingen. Het heeft drie inputs:

  1. De Actor: De geauthenticeerde gebruiker (bijv. User ID, rollen, attributen).
  2. De Actie: De bewerking die wordt uitgevoerd (bijv. read, update, delete).
  3. De Resource: Het specifieke object waartoe toegang wordt gezocht (bijv. Document ID, Organization ID).

Wanneer een resolver gegevens moet retourneren, vraagt deze de Policy Engine: "Kan Actor X Actie Y uitvoeren op Resource Z?"

Attribute-Based Access Control (ABAC)

Role-Based Access Control (RBAC) is onvoldoende om BOLA te voorkomen. BOLA gaat fundamenteel over toegang op objectniveau. Weten dat een gebruiker een "Admin" is, is nutteloos als ze proberen toegang te krijgen tot een document in een andere tenant.

We moeten Attribute-Based Access Control (ABAC) gebruiken. ABAC evalueert beleid op basis van de attributen van de actor, de resource en de omgeving.

Een beleid kan bijvoorbeeld voorschrijven: "Een gebruiker kan een document lezen ALS het organizationId van de gebruiker overeenkomt met het organizationId van het document EN het document is gemarkeerd als published."

Schema Directives

Om beleid clean af te dwingen, kunnen we GraphQL Schema Directives gebruiken. Directives stellen ons in staat om declaratieve metadata aan ons schema te koppelen. We kunnen een @auth-directive definiëren die de vereiste machtigingen voor een veld of object specificeert.

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

Deze declaratieve aanpak zorgt ervoor dat autorisatieregels zichtbaar, gedocumenteerd en consistent worden afgedwonden voordat de resolver überhaupt wordt uitgevoerd.

Implementatie: Veilige Resolvers Schrijven

Laten we een veilige implementatie bouwen met Node.js, Apollo Server v4 en een aangepaste ABAC policy engine.

Stap 1: De Policy Engine

Eerst definiëren we een eenvoudige policy engine die regels evalueert op basis van de gebruiker en de resource.

// 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; // 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') {
      // Users can only read their own salary, unless they are HR
      return user.id === targetUser.id || user.role === 'HR';
    }
    if (action === 'read') {
      return user.organizationId === targetUser.organizationId;
    }
    return false;
  }
}

Stap 2: Context-initialisatie

We instantiëren de Policy Engine en injecteren deze in de GraphQL-context, samen met de geauthenticeerde gebruiker.

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

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

export const context = async ({ req }): Promise<GraphQLContext> => {
  const user = authenticateRequest(req); // bijv. JWT verifiëren
  return {
    user,
    policyEngine: new PolicyEngine(),
  };
};

Stap 3: Veilige Resolvers

Nu gebruiken we in onze resolvers de Policy Engine om autorisatie op objectniveau af te dwingen voordat we de gegevens retourneren.

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

      // Object-Level Authorization Check
      if (!context.policyEngine.can(context.user, 'read', { __typename: 'Document', ...doc })) {
        throw new Error('Unauthorized');
      }

      return doc;
    },
  },
  User: {
    salary: (parent, _, context: GraphQLContext) => {
      // parent is het User-object dat wordt resolved
      if (!context.policyEngine.can(context.user, 'read:salary', { __typename: 'User', ...parent })) {
         throw new Error('Unauthorized to view salary');
      }
      return parent.salary;
    }
  }
};

Door de autorisatielogica naar een speciale Policy Engine te verplaatsen en de toegang expliciet te controleren na het ophalen van het object maar vóór het retourneren ervan, elimineren we de BOLA-kwetsbaarheid.

Veelvoorkomende Valkuilen en Randgevallen

Zelfs met een sterke architectuur zijn er valkuilen die je moet vermijden.

1. Het N+1-autorisatieprobleem

Als je een lijst van 100 documenten opvraagt, en je resolver controleert de autorisatie voor elk document afzonderlijk, trigger je mogelijk 100 databasequeries om de benodigde attributen voor de policy engine op te halen. Dit is het N+1-probleem toegepast op autorisatie.

Oplossing: Gebruik DataLoaders om het ophalen van autorisatie-metadata te batchen. Of nog beter: schuif de autorisatie-filters door naar de databaselaag. In plaats van 100 documenten op te halen en ze te filteren in Node.js, wijzig je de SQL-query om alleen documenten te retourneren die de gebruiker mag zien: SELECT * FROM documents WHERE organization_id = ?.

2. Gegevenslekken Via Fouten

Wanneer een gebruiker toegang probeert te krijgen tot een resource die hij niet mag zien, bevestigt het retourneren van een foutmelding zoals Unauthorized to access document 123 dat document 123 bestaat. Dit is een informatielink (information leak).

Oplossing: Als een aanvaller probeert toegang te krijgen tot een object waartoe hij geen toegang heeft, moet de API een generieke Not Found of null retourneren, precies alsof het object niet bestaat. Retourneer alleen Unauthorized als de gebruiker weet dat het object bestaat, maar de specifieke machtiging mist om de actie uit te voeren.

3. Mutaties Negeren

Ontwikkelaars beveiligen hun Queries vaak grondig, maar verwaarlozen Mutations. Een BOLA-kwetsbaarheid in een updateUser-mutatie is wellicht gevaarlijker dan een in een user-query.

Oplossing: Mutaties vereisen exact dezelfde, zo niet strengere, ABAC-controles. Verifieer of de gebruiker toestemming heeft om het specifieke object bij te werken voordat de database-schrijfopdracht wordt uitgevoerd.

4. Aannames over Client-Side Status

Vertrouw er nooit op dat de client autorisatiecontext aanlevert. Als je mutatie input: { id: "123", organizationId: "456", newData: "..." } accepteert, kun je er niet op vertrouwen dat de gebruiker daadwerkelijk tot organizationId "456" behoort. Je moet het object uit de database ophalen en de werkelijke status verifiëren tegen het geauthenticeerde token van de gebruiker.

Het Resultaat: Een Geharde GraphQL-laag

Het aanpakken van BOLA-kwetsbaarheden in GraphQL-API's is niet een functie die je aan het einde van een sprint kunt toevoegen. Het vereist een fundamentele verschuiving in hoe je je applicatie ontwerpt. Door ad-hoc resolver-controles los te laten ten gunste van een gecentraliseerde Policy Engine, ABAC te adopteren en een fail-closed paradigma af te dwingen, transformeer je je GraphQL-API van een enorm aanvalsoppervlak tot een geharde, voorspelbare interface.

Stop met vertrouwen op onduidelijkheid (security through obscurity). Stop met het vertrouwen van de client. Ga ervan uit dat elke query vijandig is, evalueer elke node-uitvoering tegen strikt beleid en bouw een API die zichzelf verdedigt. De kosten van het implementeren van robuuste autorisatie op objectniveau zijn hoog, maar de kosten van een BOLA-lek zijn catastrofaal. Schrijf veilige resolvers.

Seven Labs Dienst

VAPT Penetratietesten & Cybersecurity

Wij testen systemen op kwetsbaarheden. Zie onze beveiligingsdiensten →
Loading...

Lees volgende

Security Challenges in Distributed AI Architectures

An in-depth guide to securing distributed edge-to-cloud AI systems. Learn about key protection, ECDH...

Lees artikel

The AI Engineer Shortage and How to Outsource Smartly

The AI engineer shortage is crippling ambitious roadmaps. Here is exactly how to outsource smartly, ...

Lees artikel
Chat with us