Termin buchenKontakt
Zurück zu allen Notizen
1. Juni 2026

BOLA-Schwachstellen in GraphQL-APIs: Die stille Bedrohung

queryUserAccountProfile!BOLA

BOLA-Schwachstellen in GraphQL-APIs: Die stille Bedrohung

Broken Object Level Authorization (BOLA) bleibt die kritischste Schwachstelle moderner Webanwendungen. Wenn wir zu graphbasierten Architekturen migrieren, verändert sich die Angriffsfläche nicht nur, sie vergrößert sich auch auf unvorhersehbare Weise. Dieser Beitrag untersucht BOLA-Schwachstellen in GraphQL-APIs und analysiert genau, warum traditionelle REST-zentrierte Autorisierungsansätze katastrophal scheitern, wenn sie auf Graphen angewendet werden. Wir werden das Problem zerlegen, analysieren, warum es schwer zu lösen ist, eine robuste Architektur vorschlagen, eine konkrete Implementierung bereitstellen und die Fallstricke besprechen, auf die Sie zwangsläufig stoßen werden.

Wenn Sie GraphQL wie einen schicken REST-Endpunkt behandeln, ist Ihre API bereits verwundbar. Es ist Zeit, Ihre Autorisierungsschicht zu reparieren.

Das Problem: Autorisierung auf Objektebene ist fehlerhaft

In einer traditionellen REST-Architektur ist die Autorisierung relativ einfach, da die Endpunkt-Topologie sauber auf die Daten-Topologie abgebildet werden kann. Sie fordern GET /api/users/123/financials an. Der Router fängt die Anfrage ab, führt eine Middleware aus, die prüft, ob der authentifizierte Benutzer berechtigt ist, auf die Finanzdaten des Benutzers 123 zuzugreifen, und lässt die Anfrage entweder zu oder lehnt sie ab. Die Grenzen sind fest, gut definiert und leicht überprüfbar.

GraphQL zerstört diese Einfachheit.

Bei GraphQL bestimmt der Client die Form der Antwort. Der Endpunkt ist immer /graphql, und der Request-Body enthält eine Abfrage, die mehrere Entitäten, Beziehungen und Tiefen durchlaufen kann. Eine einzige Abfrage kann auf das Profil eines Benutzers, seine Freunde, die Beiträge der Freunde und die Kommentare zu diesen Beiträgen zugreifen.

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

Im obigen Beispiel beginnt der Angreifer bei einem Objekt, das er kontrolliert (user), wechselt zu einem gemeinsam genutzten Objekt (organization) und fordert dann ein sensibles Feld (salary) für alle Mitglieder dieser Organisation an. Wenn die Autorisierungslogik nur prüft, ob der Benutzer der Organisation angehört, aber nicht prüft, ob der Benutzer das salary-Feld anderer Mitglieder lesen darf, liegt eine BOLA-Schwachstelle vor.

Das Kernproblem besteht darin, dass GraphQL-Resolver unabhängig voneinander ausgeführt werden. Der salary-Resolver weiß, dass er einen Float für einen bestimmten Benutzer zurückgeben muss, aber ihm fehlt oft der Kontext, wer fragt und wie er zu diesem Knoten gelangt ist. Dieser Kontextverlust ist der Nährboden für BOLA.

Warum BOLA-Schwachstellen in GraphQL-APIs schwer zu finden sind

BOLA-Schwachstellen in GraphQL-APIs sind durch automatisierte Scans oder einfache Code-Reviews bekanntermaßen schwer zu identifizieren. Hier ist der Grund:

1. Tiefe und Graph-Traversierung

Angreifer fragen nicht nur die Stammknoten (Root Nodes) ab; sie durchqueren den Graphen, um Überprüfungen auf Stammebene zu umgehen. Eine Stammabfrage für users(id: "123") mag geschützt sein, aber was ist mit post(id: "456") { author { id email } }? Wenn der author-Resolver nicht genau dieselben Autorisierungsprüfungen wie die Stammabfrage user durchführt, sind die Daten ungeschützt. Der Graph ist miteinander verbunden, was bedeutet, dass es Dutzende von Pfaden zum selben Datenknoten gibt. Die Sicherung eines Pfades ist nutzlos, wenn die anderen offen bleiben.

2. Kontext-Fragmentierung

In vielen Frameworks (wie Apollo Server oder Yoga) sind Resolver reine Funktionen, die parent, args, context und info empfangen. Entwickler platzieren Autorisierungslogik häufig im context-Setup (z. B. beim Extrahieren der Benutzer-ID aus einem JWT), versäumen es jedoch, genügend Geschäftskontext an die Resolver auf Feldebene weiterzugeben. Bis die Ausführung einen tief verschachtelten Bereich erreicht, verfügt der Resolver nur noch über das übergeordnete Objekt und die Benutzer-ID. Ihm fehlen die komplexen Geschäftsregeln, die für eine genaue Autorisierungsentscheidung erforderlich sind.

3. Der Standard „Fail-Open“

GraphQL-Frameworks führen standardmäßig jeden Resolver aus, wenn das Schema dies zulässt. Sofern Sie nicht explizit Code schreiben, um den Zugriff zu verweigern, ist die Anfrage zulässig. Dieses „Fail-Open“-Paradigma ist von Natur aus gefährlich. Sichere Systeme müssen im Fehlerfall geschlossen sein („fail-closed“). Wenn eine Autorisierungsrichtlinie fehlt, muss die Anfrage abgelehnt werden.

4. Over-fetching und Maskierung

In REST fordert ein Angreifer einen bestimmten Endpunkt an, wodurch seine Absicht in den Protokollen offensichtlich wird. In GraphQL kann ein Angreifer einen BOLA-Exploit tief in einer legitim aussehenden Abfrage vergraben, die Dutzende sicherer Felder abruft. Die böswillige Nutzlast wird durch das Rauschen maskiert, was Intrusion Detection Systeme (IDS) und Web Application Firewalls (WAF) für den Angriff völlig blind macht.

Architektonische Lösungen für die GraphQL-Autorisierung

Um BOLA zu eliminieren, müssen wir uns von Ad-hoc-Autorisierungsprüfungen verabschieden, die in unseren Resolvern verstreut sind. Wir benötigen eine zentralisierte, deterministische und im Fehlerfall geschlossene (fail-closed) Autorisierungsarchitektur.

Der Policy-Engine-Ansatz

Die robusteste Architektur entkoppelt die Autorisierungslogik vollständig von den GraphQL-Resolvern. Resolver sollten nicht wissen, wie Berechtigungen ausgewertet werden; sie sollten nur wissen, wen sie fragen müssen. Dies bedeutet die Einführung einer dedizierten Policy Engine.

Die Policy Engine fungiert als einzige Quelle der Wahrheit (Single Source of Truth) für alle Autorisierungsentscheidungen. Sie benötigt drei Eingaben:

  1. Der Actor: Der authentifizierte Benutzer (z. B. Benutzer-ID, Rollen, Attribute).
  2. Die Action: Die durchgeführte Operation (z. B. read, update, delete).
  3. Die Resource: Das spezifische Objekt, auf das zugegriffen wird (z. B. Dokumenten-ID, Organisations-ID).

Wenn ein Resolver Daten zurückgeben muss, fragt er die Policy Engine ab: „Kann Actor X die Action Y auf Resource Z ausführen?“

Attribute-Based Access Control (ABAC)

Role-Based Access Control (RBAC) reicht nicht aus, um BOLA zu verhindern. Bei BOLA geht es grundlegend um den Zugriff auf Objektebene. Zu wissen, dass ein Benutzer ein „Admin“ ist, ist nutzlos, wenn er versucht, auf ein Dokument in einem anderen Mandanten (Tenant) zuzugreifen.

Wir müssen Attribute-Based Access Control (ABAC) verwenden. ABAC bewertet Richtlinien basierend auf den Attributen des Akteurs, der Ressource und der Umgebung.

Eine Richtlinie könnte beispielsweise vorschreiben: „Ein Benutzer darf ein Dokument lesen, WENN die organizationId des Benutzers mit der organizationId des Dokuments übereinstimmt UND das Dokument als published markiert ist.“

Schema-Direktiven

Um Richtlinien sauber durchzusetzen, können wir GraphQL-Schema-Direktiven verwenden. Direktiven ermöglichen es uns, deklarative Metadaten an unser Schema anzuhängen. Wir können eine @auth-Direktive definieren, die die erforderlichen Berechtigungen für ein Feld oder ein Objekt angibt.

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

Dieser deklarative Ansatz stellt sicher, dass Autorisierungsregeln sichtbar, dokumentiert und konsistent durchgesetzt werden, noch bevor der Resolver ausgeführt wird.

Implementierung: Sichere Resolver schreiben

Lassen Sie uns eine sichere Implementierung mit Node.js, Apollo Server v4 und einer benutzerdefinierten ABAC Policy Engine erstellen.

Schritt 1: Die Policy Engine

Zuerst definieren wir eine einfache Policy Engine, die Regeln basierend auf dem Benutzer und der Ressource auswertet.

// 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') {
      // Benutzer können nur ihr eigenes Gehalt lesen, es sei denn, sie sind in der Personalabteilung (HR)
      return user.id === targetUser.id || user.role === 'HR';
    }
    if (action === 'read') {
      return user.organizationId === targetUser.organizationId;
    }
    return false;
  }
}

Schritt 2: Kontext-Initialisierung

Wir instanziieren die Policy Engine und injizieren sie zusammen mit dem authentifizierten Benutzer in den GraphQL-Kontext.

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

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

export const context = async ({ req }): Promise<GraphQLContext> => {
  const user = authenticateRequest(req); // z.B. JWT verifizieren
  return {
    user,
    policyEngine: new PolicyEngine(),
  };
};

Schritt 3: Sichere Resolver

In unseren Resolvern verwenden wir nun die Policy Engine, um die Autorisierung auf Objektebene durchzusetzen, bevor wir Daten zurückgeben.

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

      // Überprüfung der Autorisierung auf Objektebene
      if (!context.policyEngine.can(context.user, 'read', { __typename: 'Document', ...doc })) {
        throw new Error('Unauthorized');
      }

      return doc;
    },
  },
  User: {
    salary: (parent, _, context: GraphQLContext) => {
      // parent ist das User-Objekt, das aufgelöst wird
      if (!context.policyEngine.can(context.user, 'read:salary', { __typename: 'User', ...parent })) {
         throw new Error('Unauthorized to view salary');
      }
      return parent.salary;
    }
  }
};

Indem wir die Autorisierungslogik in eine dedizierte Policy Engine auslagern und den Zugriff explizit prüfen, nachdem wir das Objekt abgerufen haben, aber bevor wir es zurückgeben, eliminieren wir die BOLA-Schwachstelle.

Häufige Fallstricke und Edge Cases

Selbst mit einer starken Architektur gibt es Fallstricke, die Sie vermeiden müssen.

1. Das N+1 Autorisierungsproblem

Wenn Sie eine Liste von 100 Dokumenten abfragen und Ihr Resolver die Autorisierung für jedes Dokument einzeln prüft, lösen Sie unter Umständen 100 zusätzliche Datenbankabfragen aus, nur um die erforderlichen Attribute für die Policy Engine abzurufen. Dies ist das N+1-Problem angewendet auf die Autorisierung.

Lösung: Verwenden Sie DataLoaders, um das Abrufen von Autorisierungsmetadaten zu bündeln. Noch besser ist es, die Autorisierungsfilter auf die Datenbankebene zu verlagern. Anstatt 100 Dokumente abzurufen und sie in Node.js zu filtern, ändern Sie die SQL-Abfrage so, dass nur Dokumente zurückgegeben werden, die der Benutzer sehen darf: SELECT * FROM documents WHERE organization_id = ?.

2. Datenlecks durch Fehlermeldungen

Wenn ein Benutzer versucht, auf eine Ressource zuzugreifen, auf die er keinen Zugriff hat, bestätigt die Rückgabe einer Fehlermeldung wie Unauthorized to access document 123, dass das Dokument 123 existiert. Dies ist ein Informationsleck.

Lösung: Wenn ein Angreifer versucht, auf ein Objekt zuzugreifen, für das er keine Sichtbarkeit besitzt, sollte die API ein generisches Not Found oder null zurückgeben, genau so, als ob das Objekt nicht existieren würde. Geben Sie Unauthorized nur zurück, wenn der Benutzer weiß, dass das Objekt existiert, aber ihm die spezifische Berechtigung zur Durchführung der Aktion fehlt.

3. Ignorieren von Mutationen

Entwickler sichern oft ihre Queries intensiv ab, vernachlässigen jedoch Mutationen. Eine BOLA-Schwachstelle in einer updateUser-Mutation ist wohl weitaus gefährlicher als eine in einer user-Abfrage.

Lösung: Mutationen erfordern dieselben, wenn nicht sogar strengere ABAC-Prüfungen. Überprüfen Sie, ob der Benutzer die Berechtigung hat, das spezifische Objekt zu aktualisieren, bevor Sie den Schreibvorgang in der Datenbank ausführen.

4. Annahmen über den clientseitigen Zustand

Vertrauen Sie niemals darauf, dass der Client den Autorisierungskontext bereitstellt. Wenn Ihre Mutation input: { id: "123", organizationId: "456", newData: "..." } akzeptiert, können Sie nicht darauf vertrauen, dass der Benutzer tatsächlich zur organizationId „456“ gehört. Sie müssen das Objekt aus der Datenbank abrufen und seinen tatsächlichen Zustand mit dem authentifizierten Token des Benutzers abgleichen.

Das Ergebnis: Eine gehärtete GraphQL-Schicht

Die Behebung von BOLA-Schwachstellen in GraphQL-APIs ist keine Funktion, die Sie am Ende eines Sprints schnell hinzufügen können. Es erfordert ein grundlegendes Umdenken beim Entwurf Ihrer Anwendung. Indem Sie Ad-hoc-Resolver-Prüfungen zugunsten einer zentralen Policy Engine aufgeben, ABAC einführen und ein Fail-Closed-Paradigma durchsetzen, verwandeln Sie Ihre GraphQL-API von einer massiven Angriffsfläche in eine gehärtete, berechenbare Schnittstelle.

Hören Sie auf, sich auf Unklarheit zu verlassen. Vertrauen Sie dem Client nicht. Gehen Sie davon aus, dass jede Abfrage feindselig ist, bewerten Sie jede Knotenausführung anhand strenger Richtlinien und bauen Sie eine API, die sich selbst verteidigt. Die Kosten für die Implementierung einer robusten Autorisierung auf Objektebene sind hoch, aber die Kosten einer BOLA-Sicherheitsverletzung sind katastrophal. Schreiben Sie sichere Resolver.

Seven Labs Dienstleistung

VAPT-Penetrationstests & Cybersicherheit

Wir testen Systeme auf Sicherheitslücken. Siehe unsere Sicherheitsdienste →
Loading...

Nächstes lesen

How to Run an AI Proof of Concept Without Committing Your Entire Engineering Team

An AI proof of concept shouldn't paralyze your core product roadmap. Here is how CTOs can test gener...

Artikel lesen

AI Development Partner Evaluation: What to Demand Before You Sign

A practical framework for AI development partner evaluation. Learn how to spot vendor red flags, mit...

Artikel lesen
Chat with us