Zum Inhalt springen

Unzureichende Autorisierung auf Objektebene in APIs

CWE-639CWE-285OWASP A01:2021Aktualisiert 4. September 20265 Min. Lesezeit

Bei unzureichender Autorisierung auf Objektebene prüft eine API zwar, ob der Anfragende angemeldet ist, nicht aber, ob das angeforderte Objekt ihm gehört. Durch Ändern der Kennung in der URL gelangt ein gültiger Nutzer so an die Daten anderer. Es ist die häufigste und am meisten ausgenutzte Schwachstelle in APIs.

Von allen Schwachstellen in APIs ist dies die häufigste und zugleich die unauffälligste: Es geht nichts kaputt, es erscheint keine Fehlermeldung, und die Anfrage ist technisch völlig korrekt. Nur erhält der Anfragende Daten, die ihm nicht gehören. Im Folgenden lesen Sie, warum dieser Fehler so hartnäckig ist und wie Sie ihn strukturell ausschließen.

Was ist Autorisierung auf Objektebene?

Bei jeder Anfrage an eine API müssen zwei verschiedene Fragen beantwortet werden. Die erste lautet: Wer sind Sie? Das ist die Authentifizierung, und sie wird fast immer sauber geregelt. Die zweite lautet: Dürfen Sie auf genau dieses Objekt zugreifen? Das ist die Autorisierung auf Objektebene, und sie wird regelmäßig übersprungen.

Unzureichende Autorisierung auf Objektebene, international als Broken Object Level Authorization oder BOLA bekannt, bedeutet, dass die API das Erste prüft und das Zweite nicht. Jeder mit einem gültigen Konto gelangt dann an die Daten jedes anderen Kontos, schlicht durch Ändern der Kennung in der Anfrage.

Denken Sie an eine Poststelle, in der Sie sich beim Eintritt ordentlich ausweisen und anschließend jedes Fach öffnen dürfen, auf das Sie zeigen. Der Ausweis war echt, der Mitarbeiter hat seine Arbeit getan, und dennoch stehen Sie in fremder Post. Die fehlende Prüfung lautet nicht “wer sind Sie”, sondern “gehört dieses Fach Ihnen”.

Wie wird fehlende Autorisierung auf Objektebene ausgenutzt?

Verwundbar:

// Es wird geprüft, ob jemand angemeldet ist, und sonst nichts
app.get('/api/rechnungen/:id', erfordertAnmeldung, async (req, res) => {
  const rechnung = await db.rechnungen.finde(req.params.id);
  if (!rechnung) return res.sendStatus(404);

  res.json(rechnung);
});

Die Middleware erfordertAnmeldung tut genau das, was ihr Name sagt: Sie weist Anfragen ohne gültige Sitzung ab. Was sie nicht tut, ist festzustellen, ob diese Rechnung zu diesem Nutzer gehört. Ein angemeldeter Kunde muss also nur die Nummer ändern:

GET /api/rechnungen/10432 HTTP/1.1     ← eigene Rechnung
GET /api/rechnungen/10433 HTTP/1.1     ← Rechnung eines anderen Kunden
HTTP/1.1 200 OK

Beide Anfragen gelingen. Und da es sich um eine API handelt, ist dies trivial zu automatisieren: Eine Schleife über alle Nummern liefert die vollständige Rechnungsverwaltung. Fehlt zusätzlich eine Begrenzung der Anfragerate, ist ein vollständiges Datenleck eine Sache von Minuten, ausgeführt mit einem gültigen Konto und ohne eine einzige Fehlermeldung in Ihren Protokollen.

Sicher:

// Der Eigentümer ist Teil der Abfrage, nicht einer nachträglichen Prüfung
app.get('/api/rechnungen/:id', erfordertAnmeldung, async (req, res) => {
  const rechnung = await db.rechnungen.findeFuerKunde(
    req.params.id,
    req.nutzer.kundeId,           // aus der Sitzung, nie aus der Anfrage
  );
  if (!rechnung) return res.sendStatus(404);   // gleiche Antwort wie 'existiert nicht'

  res.json(rechnung);
});

Der Unterschied ist im Code klein und in der Folge groß. Der Eigentümer steckt nun in der Abfrage selbst, und dieser Eigentümer stammt aus der Sitzung, nicht aus einem Parameter, den der Anfragende ändern kann. Existiert die Rechnung zwar, gehört sie aber jemand anderem, kommt nichts zurück.

Beachten Sie die gewählte Antwort: 404 und nicht 403. Ein 403 bestätigt, dass das Objekt existiert, und das ist für sich genommen bereits eine Information, mit der ein Angreifer feststellen kann, welche Kennungen in Gebrauch sind.

Bei einer wachsenden Anwendung ist es auf Dauer nicht haltbar, diese Prüfung je Endpunkt zu wiederholen. Robuster ist es, sie an einer Stelle zu erzwingen, die sich nicht vergessen lässt:

// Jede Abfrage über dieses Repository führt den Eigentümer automatisch mit
const rechnungenFuer = (nutzer) => ({
  finde: (id) => db.rechnungen.erste({ id, kunde_id: nutzer.kundeId }),
  liste: () => db.rechnungen.alle({ kunde_id: nutzer.kundeId }),
});
Vertrauen Sie nie einer Eigentümerkennung, die in der Anfrage selbst steht. Ein Parameter wie kundeId oder ein Feld im JSON-Körper wird vom Anfragenden bestimmt und ist damit kein Eigentumsnachweis. Die einzige verlässliche Quelle ist die geprüfte Sitzung oder das geprüfte Token.

Welche Auswirkungen hat fehlende Autorisierung auf Objektebene?

Der Schweregrad ist hoch bis kritisch, und bei diesem Befund ist das fast immer berechtigt. Der Grund liegt in der Verbindung von Einfachheit und Umfang.

Der Angriff erfordert kein besonderes Wissen: Ein gültiges Konto und das Ändern einer Zahl genügen. Es braucht keine Einschleusung, keinen Umweg und keinen Exploit. Und da APIs für den maschinellen Gebrauch entworfen sind, lässt sich der Angriff mühelos zu einem vollständigen Auslesen des Datenbestands automatisieren. Viele große Datenlecks bei mobilen Anwendungen und Kundenportalen sind genau auf diesem Weg entstanden.

Betrifft es Endpunkte, die auch schreiben, bleibt es nicht beim Lesen. Ein PUT oder DELETE ohne Eigentümerprüfung bedeutet, dass ein Nutzer die Daten anderer ändern oder löschen kann. Bei einem Endpunkt, der Rollen oder Rechte anpasst, steht der Weg zur Erhöhung der eigenen Rechte offen.

Was die Erkennung erschwert: Es ist nichts Abweichendes zu sehen. Jede Anfrage ist authentifiziert, korrekt aufgebaut und mit einem 200 beantwortet. Ohne Beobachtung des Musters, also eines Kontos, das Hunderte verschiedener Objekte abruft, fällt es nicht auf.

Wie erkennt man fehlende Autorisierung auf Objektebene?

Der Test verlangt zwei Konten und Systematik. Ein Tester meldet sich als Nutzer A an, geht die Anwendung durch und hält jede Anfrage fest, die eine Kennung enthält. Anschließend wiederholt er diese Anfragen mit dem Token von Nutzer B. Jede Antwort, die Daten von A zurückgibt, ist ein Befund.

Das geschieht nicht nur für das Abrufen. Gerade die ändernden und löschenden Endpunkte werden häufig vergessen, während die Folgen dort größer sind. Ebenso wird auf Kennungen geachtet, die nicht im Pfad, sondern im Körper, in einem Header oder in einem verschachtelten Objekt stehen; eine bestellungId tief in einer JSON-Struktur wird selten geprüft. Weiter werden Endpunkte geprüft, die mehrere Objekte zugleich verarbeiten, bei denen die Prüfung mitunter nur auf das erste Element angewandt wird. Und es wird auf Unterschiede in der Antwort geachtet: ein 403, wo sonst ein 404 kommt, verrät, welche Kennungen existieren. AssistSec führt diesen Test mit Konten in verschiedenen Rollen und zwischen verschiedenen Kunden in einer gemeinsam genutzten Umgebung durch, denn gerade die Trennung zwischen Kunden erweist sich in der Praxis am häufigsten als unvollständig.

Wie verhindert man fehlende Autorisierung auf Objektebene?

  • Prüfen Sie bei jeder Anfrage, ob das angeforderte Objekt dem authentifizierten Nutzer gehört.
  • Nehmen Sie den Eigentümer in die Datenbankabfrage selbst auf, statt nachträglich zu vergleichen.
  • Beziehen Sie die Eigentümerkennung ausschließlich aus der Sitzung oder dem Token, nie aus der Anfrage.
  • Erzwingen Sie die Prüfung zentral, etwa in einer Repository-Schicht, damit ein neuer Endpunkt sie nicht vergessen kann.
  • Antworten Sie mit 404 statt 403, damit die Existenz von Objekten nicht bestätigt wird.
  • Prüfen Sie jede Methode gesondert: Abrufen, Ändern, Löschen und Massenvorgänge.
  • Prüfen Sie auch Kennungen, die im Körper oder in verschachtelten Strukturen stehen.
  • Nutzen Sie unvorhersehbare Kennungen als zusätzliche Schicht, nie als Ersatz für Autorisierung.
  • Überwachen Sie Konten, die ungewöhnlich viele verschiedene Objekte abrufen.

Quellen

Häufige Fragen

Hilft es, UUIDs statt fortlaufender Nummern zu verwenden?

Es erschwert das Raten, löst aber nichts. Kennungen laufen über geteilte Links, Exporte, Protokolldateien und andere Endpunkte derselben Anwendung aus. Sobald ein Angreifer eine davon kennt, funktioniert der Angriff unverändert. Unvorhersehbare Kennungen sind eine nützliche Ergänzung, keine Autorisierung.

Warum kommt das bei APIs so häufig vor?

Weil eine API oft je Ressource aufgebaut ist und die Autorisierung je Endpunkt wiederholt werden muss. Ein einziger neuer Endpunkt, bei dem die Prüfung vergessen wurde, genügt. In einer Weboberfläche wird es zudem verdeckt, da der Nutzer nur seine eigenen Links sieht; bei einer API fehlt dieser Schutzschild.

Was unterscheidet es von BFLA?

Bei BOLA geht es um die Frage, ob Sie auf dieses Objekt zugreifen dürfen; bei BFLA darum, ob Sie diese Funktion überhaupt ausführen dürfen. Ersteres ist ein gewöhnlicher Nutzer, der an einen fremden Datensatz gelangt, Letzteres ein gewöhnlicher Nutzer, der eine Verwaltungsfunktion aufruft. Beides muss gesondert geprüft werden.

Wie prüfe ich das planmäßig?

Mit zwei Konten. Führen Sie als Nutzer A einen Vorgang aus, halten Sie die Anfrage fest und wiederholen Sie sie mit dem Token von Nutzer B. Sieht B die Daten von A, steht der Befund. Tun Sie das für jeden Endpunkt, der eine Kennung annimmt, auch für Ändern und Löschen.

Verwandte Artikel

Zum Suchen / drücken · Esc