Aikido

Von der Erkennung zur Prävention: Wie Zen IDOR-Schwachstellen zur Laufzeit stoppt

Verfasst von
Hans Ott

TL:DR

IDORs sind der häufigste Weg, wie Multi-Tenant-SaaS-Unternehmen Daten preisgeben, und sie werden meist erst nach der Bereitstellung entdeckt. Aikido Zen macht die Mandantenisolation obligatorisch. Zen parst alle Ihre SQL-Abfragen zur Laufzeit mithilfe eines geeigneten SQL-Parsers (geschrieben in Rust), prüft, ob jede Abfrage nach dem richtigen Mandanten filtert, und löst einen Fehler aus, wenn dies nicht der Fall ist. Entwickelnde können mandantenübergreifenden Zugriff nicht mehr versehentlich ausliefern. Es ist heute für Node.js verfügbar, Python, PHP, Go, Ruby, Java und .NET folgen in Kürze.

Warum IDORs jetzt gefährlicher sind

IDOR-Schwachstellen, auch bekannt als Insecure Direct Object References, sind eine der häufigsten und gefährlichsten Schwachstellen in Multi-Tenant-Anwendungen. Sie treten auf, wenn eine Abfrage vergisst, nach dem Mandanten zu filtern, wodurch ein Konto auf die Daten eines anderen Kontos zugreifen kann.

Lange Zeit waren IDORs schwer zu erkennen. Sie tauchten nicht in Code-Scans auf und erforderten viel manuellen Aufwand. Deshalb wurden viele IDOR-Schwachstellen erst während teurer, aufwendiger Pentests oder durch Sicherheitsforschende über Bug-Bounty-Programme entdeckt.

Das hat sich jedoch geändert. Agentenbasierte Sicherheitstest-Tools können sich jetzt wie echte Nutzende verhalten, Workflows durchklicken, Rollen wechseln und automatisch versuchen, auf Ressourcen zuzugreifen. Dies erleichtert die Erkennung von IDOR-Schwachstellen erheblich. Doch dies ist ein zweischneidiges Schwert: Wenn diese Schwachstellen leichter zu finden sind, sind sie auch leichter auszunutzen. Deshalb sollten sich Organisationen nicht nur auf die Erkennung von IDORs konzentrieren, sondern auch auf deren Prävention.

Warum Erkennung nicht ausreicht

Auf der Erkennungsseite kann Aikidos AI Pentest bereits IDOR-Schwachstellen finden, etwas, das traditionelle musterbasierte SAST-Tools nie zuverlässig leisten konnten, da IDOR das Verständnis des Autorisierungskontexts erfordert, nicht nur Code-Muster. AI Pentest authentifiziert sich als echte Benutzer, durchläuft vollständige Workflows und verwendet Objekt-Identifikatoren rollenübergreifend wieder, um tatsächlich ausnutzbare IDOR-Schwachstellen zu finden. Aus diesem Grund sind viele Organisationen, die unsere AI Pentest-Funktion nutzen, hauptsächlich daran interessiert, IDORs zu finden.

Doch das Finden von IDOR-Schwachstellen ist nur die halbe Miete. Im Idealfall würde man deren Einführung von vornherein verhindern. Darum geht es in diesem Beitrag: IDOR-Schutz in Zen, unserer Open-Source In-App-Firewall. Sie analysiert jede SQL-Abfrage zur Laufzeit und löst einen Fehler aus, wenn einer Abfrage ein Mandantenfilter fehlt oder die falsche Mandanten-ID verwendet wird, wodurch der Fehler während der Entwicklung und des Tests abgefangen wird, bevor er jemals die Produktion erreicht.

In vielen Unternehmensumgebungen, insbesondere bei Sicherheitsüberprüfungen oder Anbieterbewertungen, ist eine wiederkehrende Frage, wie Multi-Tenancy durchgesetzt und mandantenübergreifender Datenzugriff verhindert wird.

Sicherheitsteams und Führungskräfte wünschen sich klare, technische Zusicherungen, dass Mandantengrenzen systematisch und nicht nur durch Konvention durchgesetzt werden.

Ein Mechanismus, der die Mandanten-Scoping auf Abfrageebene automatisch validiert, bietet eine einfache, glaubwürdige Antwort. Er verlagert die Diskussion von „wir verlassen uns darauf, dass Entwickelnde dies beachten“ zu „das System erzwingt es automatisch“.

So sieht das Setup aus:

import Zen from "@aikidosec/firewall";

// 1. Tell Zen which column identifies the tenant
Zen.enableIdorProtection({
  tenantColumnName: "tenant_id",
  excludedTables: ["users"],
});

// 2. Set the tenant ID per request (e.g., in middleware after authentication)
app.use((req, res, next) => {
  Zen.setTenantId(req.user.organizationId);
  next();
});

// 3. Optionally bypass for specific queries (e.g., admin dashboards)
const result = await Zen.withoutIdorProtection(async () => {
  return await db.query("SELECT count(*) FROM orders WHERE status = 'active'");
});

Wie sieht eine IDOR-Schwachstelle aus?

Wenn Ihre App Konten, Organisationen, Workspaces oder Teams hat, haben Sie wahrscheinlich eine Spalte wie tenant_id die die Daten jedes Kontos getrennt hält. Wenn die Abfrage jedoch vergisst, nach dieser Spalte zu filtern oder nach dem falschen Wert filtert, bedeutet dies, dass ein Konto auf die Daten eines anderen zugreifen kann. Dies ist eine IDOR-Schwachstelle.  

Hier ist ein einfaches Beispiel. Sie haben einen Endpunkt, der die Bestellungen eines Benutzers zurückgibt:

app.get("/orders/:orderId", async (req, res) => {
  const order = await db.query(
    "SELECT * FROM orders WHERE id = $1",
    [req.params.orderId]
  );

  res.json(order);
});

Sehen Sie das Problem? Es gibt keinen tenant_id Filter. Wenn Alice sendet GET /orders/42 und Bestellung 42 Bob gehört, erhält Alice Bobs Bestellung. Das ist eine IDOR.

Die Lösung ist einfach, fügen Sie einen WHERE tenant_id = $2 Klausel hinzu. Der Fehler ist jedoch leicht einzuführen und schwer zu erkennen, insbesondere in einer großen Codebasis mit Hunderten von Abfragen über Dutzende von Dateien hinweg. Ein einziger fehlender Filter genügt.

IDOR ist eine breite Kategorie. Es umfasst auch Dinge wie den Zugriff auf Dateien anderer Benutzer über URL-Manipulation oder API-Endpunkte, die die Eigentümerschaft nicht prüfen. Dieser Beitrag konzentriert sich speziell auf die SQL-Mandantenfilterung, um sicherzustellen, dass jede Datenbankabfrage korrekt auf den aktuellen Mandanten zugeschnitten ist. Für einen tieferen Einblick in IDOR im Allgemeinen lesen Sie unseren Beitrag „IDOR-Schwachstellen erklärt“.
Was gibt es heute auf dem Markt

Abgesehen von Sicherheitsscans, bei denen es eher darum geht, bestehende Fehler zu finden, gibt es andere Methoden, um die Einführung neuer IDORs zu verhindern: Framework-Level-Bibliotheken und Datenbank-Level-Durchsetzung. Jede hat Stärken und Einschränkungen.

Framework-Bibliotheken

Mehrere Frameworks verfügen über Bibliotheken, die Abfragen automatisch auf den aktuellen Tenant beschränken:

  • Ruby on Rails: acts_as_tenant fügt ActiveRecord-Modellen eine automatische Tenant-Beschränkung hinzu. Deklarieren Sie acts_as_tenant(:account), und alle Abfragen auf diesem Modell werden nach dem aktuellen Tenant gefiltert.
  • Django: django-multitenant tut dasselbe für Djangos ORM. Legen Sie den aktuellen Tenant in der Middleware fest, und Product.objects.all() wird automatisch Tenant-bezogen.
  • Laravel: Tenancy for Laravel bietet sowohl Single-Database- als auch Multi-Database-Multi-Tenancy mit automatischem Kontextwechsel.
  • .NET / EF Core: Globale Abfragefilter ermöglichen die Anwendung von WHERE tenant_id = X auf jede Abfrage automatisch auf Modellebene.

Diese Bibliotheken funktionieren gut innerhalb ihres eigenen ORM. Die Einschränkung ist, dass sie nur Abfragen schützen, die über die Abstraktion des ORM laufen. Raw-SQL-Abfragen, Abfragen von anderen Bibliotheken oder Abfragen, die mit einem anderen Query Builder im selben Projekt erstellt wurden, werden nicht beschränkt. Sie sind auch Opt-in, man muss daran denken, die Annotation zu jedem Modell hinzuzufügen, und neue Modelle können unbemerkt durchrutschen. Fairerweise muss man sagen, acts_as_tenant hat eine require_tenant Config, die einen Fehler auslöst, wenn kein Tenant gesetzt ist, was das Risiko, den Tenant zu vergessen, erheblich mindert.

Es gibt auch subtile Fallstricke. In Rails zum Beispiel, acts_as_tenant funktioniert, indem es einen default_scope. Wenn eine Entwickelnde aufruft Project.unscoped um einen anderen default_scope zu entfernen, wie zum Beispiel einen archiviert Filter, entfernt es alle default_scopes, einschließlich des Tenant-Filters, ohne Fehler oder Warnung. Rails hat unscope (ohne das d) zum chirurgischen Entfernen eines einzelnen Scopes, aber das erfordert, dass der Tenant-Scope überhaupt bekannt ist. In einer Codebasis mit vielen Entwickelnden wird jemand irgendwann zu unscoped, und die Tenant-Grenze verschwindet stillschweigend.

Erzwingung auf Datenbankebene

PostgreSQL Row-Level Security (RLS) geht noch einen Schritt weiter, indem es die Tenant-Isolation auf Datenbankebene erzwingt. Anstatt sich darauf zu verlassen, dass Ihre Anwendung WHERE tenant_id = ? zu jeder Abfrage hinzufügt, weisen Sie Postgres selbst an, dies zu erzwingen:

-- 1. RLS für jede Tabelle aktivieren
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- 2. Eine Policy erstellen: nur Zeilen zulassen, die der Session-Variable entsprechen
CREATE POLICY tenant_isolation ON projects
  FOR ALL
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- 3. Pro Anfrage den Tenant-Kontext vor dem Ausführen von Abfragen setzen
SET app.current_tenant_id = 'aaaa-aaaa-aaaa';

-- Jetzt gibt selbst ein einfaches SELECT nur die Zeilen dieses Tenants zurück
SELECT * FROM projects;

RLS ist die stärkste Garantie unter den hier aufgeführten Ansätzen. Ob Raw SQL oder ORM-Abfragen, es spielt keine Rolle. Die Datenbank erzwingt es. Und im Gegensatz zu acts_as_tenant, werden, wenn Sie vergessen, die Session-Variable zu setzen, keine Daten zurückgegeben, anstatt alle Daten. Das ist ein wesentlich sichererer Standard.

Es bringt jedoch echte Kompromisse mit sich. RLS wirft keine Fehler; Abfragen geben stillschweigend weniger Zeilen zurück oder beeinflussen nichts. Das ist sicherer, als alle Daten zurückzugeben, erschwert aber das Debugging. Ein UPDATE , der 100 Zeilen ändern sollte, könnte aufgrund einer Policy-Fehlkonfiguration stillschweigend 0 Zeilen beeinflussen, und es ist schwierig, dies von „keine Daten vorhanden“ zu unterscheiden.

Auch Connection Pooling erhöht die Komplexität. RLS mit SET funktioniert nicht richtig mit pgBouncer im Statement- oder Transaction-Pooling-Modus, was dazu führen kann, dass Zeilen für den falschen Tenant zurückgegeben werden. Dies kann erst in der Produktion zum Vorschein kommen.

Es gibt auch strukturelle Einschränkungen. Superuser umgehen alle Policies vollständig, und Views umgehen RLS standardmäßig, daher muss Ihre Anwendung sich als Nicht-Superuser-Rolle verbinden. Schließlich ist es nur für Postgres. Wenn Sie MySQL, SQLite für die Entwicklung oder einen anderen Datenspeicher unterstützen müssen, ist Ihre Sicherheitsebene nicht dabei.

Das pragmatische Fazit: RLS ist hervorragend als Sicherheitsnetz für die Tenant-Isolation geeignet, aber die operative Komplexität und die Schwierigkeit beim Debugging bedeuten, dass es keine sofort einsatzbereite Lösung für jedes Team ist.

Wo Zen ins Spiel kommt

Dies sind alles gültige Ansätze, und wenn Sie einen davon verwenden, ist das großartig. Der IDOR-Schutz von Zen ist für ein anderes Szenario konzipiert: Ihre Abfragen laufen über einen Datenbanktreiber, direkt oder über ein ORM, und Sie möchten ein Sicherheitsnetz, das unabhängig davon funktioniert, welches ORM, welcher Query Builder oder welches Raw SQL-Muster Sie verwenden, ohne Ihre Datenbankkonfiguration zu ändern oder eine bestimmte Framework-Bibliothek zu übernehmen.

Zen hat, ehrlich gesagt, seine eigenen Kompromisse. Wie acts_as_tenant, erfordert es, dass Sie setTenantId bei jeder Anfrage aufrufen. Wenn Sie es vergessen, wirft Zen einen Fehler, sodass es laut und nicht stillschweigend fehlschlägt, aber es ist dieselbe Art von Setup pro Anfrage. Und im Gegensatz zu RLS deckt Zen nur Abfragen ab, die innerhalb Ihrer Anwendung ausgeführt werden. Wenn sich jemand direkt mit der Datenbank verbindet, zum Beispiel über psql oder einen separaten Dienst ohne Zen, werden diese Abfragen nicht überprüft.

Es ist zudem designbedingt sprachunabhängig. Da die SQL-Analyse-Engine in Rust geschrieben ist, kompilieren wir sie zu WebAssembly für Node.js und Go sowie zu einer nativen Bibliothek, die andere Agents über FFI aufrufen. Der IDOR-Schutz wird speziell auch für die Python-, PHP-, Go-, Ruby-, Java- und .NET-Agents verfügbar sein.

Wie Zen vor IDORs schützt

Zen läuft innerhalb Ihrer Anwendung und analysiert SQL-Abfragen zur Laufzeit, mit vollständigem Kontext darüber, wer die Anfrage stellt.

Ein vollwertiger SQL-Parser, in Rust geschrieben

Im Zentrum des IDOR-Schutzes von Zen steht ein echter SQL-Parser, der auf dem sqlparser-Crate in Rust basiert und zu WebAssembly für Node.js und Go kompiliert wird. Er parst SQL auf die gleiche Weise wie eine Datenbank, indem er einen vollständigen Abstract Syntax Tree (AST) Ihrer Abfrage erstellt und diesen dann durchläuft, um Folgendes zu extrahieren:

  • Welche Tabellen die Abfrage betrifft (einschließlich Aliase)
  • Welche Gleichheitsfilter in der WHERE-Klausel vorhanden sind
  • Welche Spalten und Werte in INSERT-Anweisungen enthalten sind

Warum kein Regex? Regex funktioniert gut für einfache Abfragen wie SELECT * FROM orders WHERE tenant_id = ?. Aber reale Anwendungen verwenden CTEs, UNIONs, Subqueries, JOINs mit Aliasen und alle möglichen gültigen SQL-Konstrukte, mit denen ein Regex-basierter Ansatz Schwierigkeiten hat. Mit zunehmender Komplexität der Abfragen wird das Regex-basierte Parsing immer anfälliger. Es ist nicht unbedingt falsch, aber schwer zu warten und birgt ein hohes Überraschungspotenzial.

Ein vollwertiger Parser bewältigt all dies von Haus aus. Er erkennt auch korrekt Anweisungen, die keiner Überprüfung bedürfen, wie z.B. DDL-Anweisungen (CREATE TABLE, ALTER TABLE)BEGIN, COMMIT, ROLLBACK), Transaktionskontrolle (SET, SHOW).

) und Session-Befehle (

So sieht die Analyse unter der Haube aus. Angesichts dieser Abfrage: SELECT * FROM orders
LEFT JOIN order_items ON orders.id = order_items.order_id
WHERE orders.tenant_id = $1
AND orders.status = 'active';

Der Parser erzeugt:

[
  {
    "kind": "select",
    "tables": [
      { "name": "orders" },
      { "name": "order_items" }
    ],
    "filters": [
      { "table": "orders", "column": "tenant_id", "value": "$1" },
      { "table": "orders", "column": "status", "value": "active" }
    ]
  }
]

Zen prüft dann, ob jede Tabelle in der Abfrage einen Filter auf tenant_idhat und ob der Filterwert dem aktuellen Tenant entspricht.

Dasselbe gilt für INSERT, UPDATE, und DELETE. Zen stellt sicher, dass die Tenant-Spalte immer vorhanden ist und immer den richtigen Wert hat. Diese werden als Fehler ausgegeben, nicht nur protokolliert. IDOR ist ein Entwickelnden-Bug, kein externer Angriff, daher soll er während der Entwicklung und des Testens lautstark auffallen, anstatt unbemerkt in die Produktion zu gelangen.

Performance

Das Parsen von SQL bei jeder Abfrage klingt aufwendig, ist aber in der Praxis schnell. Die entscheidende Erkenntnis ist, dass die meisten Anwendungen Prepared Statements oder parametrisierte Abfragen verwenden. Der SQL-String bleibt gleich, nur die Parameterwerte ändern sich. Daher SELECT * FROM orders WHERE tenant_id = $1 AND status = $2 wird einmal geparst, und jede nachfolgende Ausführung derselben Abfrage ist ein Cache-Hit.

Wenn Zen zum ersten Mal einen neuen Abfrage-String sieht, erstellt der Rust-Parser den AST und extrahiert die Tabellen und Filter. Danach ist es lediglich ein Cache-Lookup und ein Vergleich der Tenant-ID mit dem aufgelösten Platzhalterwert.

Wenn Sie Werte direkt in SQL-Strings einbetten, zum Beispiel durch String-Verkettung anstelle von parametrisierten Abfragen, erfordert jeder einzigartige String ein neues Parsen. Das sollten Sie aber ohnehin vermeiden. Parametrisierte Abfragen schützen vor SQL-Injection und beschleunigen zudem die IDOR-Prüfung.

Der Weg in die Produktion: Unser eigenes Dogfooding

Wir haben Zens IDOR-Schutz auf mehreren internen Diensten von Aikido eingesetzt. Dies deckte sofort Randfälle auf, die behandelt werden mussten.

Die Transaktionsunterstützung war anfangs ein Blocker. Echte Anwendungen verwenden BEGIN, COMMIT, und ROLLBACK, und Zen musste diese als sichere Anweisungen erkennen, die kein Tenant-Filtering erfordern, anstatt Fehler bei ihnen auszugeben. Wir haben dies schnell hinzugefügt, nachdem wir gesehen hatten, dass es bei unserer ersten internen Bereitstellung fehlschlug.

Common Table Expressions (CTEs) waren eine weitere Herausforderung. Eine CTE wie WITH active AS (SELECT * FROM orders WHERE tenant_id = $1) erstellt eine virtuelle Tabelle, auf die nachfolgende Abfragen verweisen. Zen musste CTE-Namen verfolgen und sie von der Liste der „echten Tabellen“ ausschließen, während der CTE-Body weiterhin auf korrekte Filterung analysiert wurde.

Der withoutIdorProtection Die Escape-Hatch erwies sich ebenfalls als unerlässlich. Nicht jede Abfrage benötigt Tenant-Filtering, wie z.B. Admin-Dashboards, Hintergrundjobs oder mandantenübergreifende Analysen. Wir haben anfangs einen ignoreNextQuery Ansatz versucht, bei dem man vor der Abfrage eine Funktion aufrufen würde, um die Prüfung für die nächste SQL-Anweisung zu überspringen:

Zen.ignoreNextQuery();
const result = await db.query("SELECT count(*) FROM orders");

Dies erwies sich in der Praxis als fragil. Bei Verbindungspools ist die „nächste Abfrage“ auf einer bestimmten Verbindung möglicherweise nicht diejenige, die Sie überspringen wollten. Der Callback-basierte withoutIdorProtection ist explizit im Umfang. Der IDOR-Schutz ist für die Dauer des Callbacks und nichts anderes deaktiviert.

Wie wir unsere API geschützt haben, die Cloud-Asset-Daten bereitstellt

Einer der Dienste, die wir frühzeitig geschützt haben, war die interne API, die Cloud-Asset-Daten plattformübergreifend bereitstellt.

Diese API wird von der UI, Hintergrundjobs und mehreren Security Engines verwendet, wenn diese Informationen über die Infrastruktur eines Kunden lesen müssen. Sie bildet den Kern des Systems und verarbeitet Tausende von Anfragen pro Sekunde.

Da die Plattform vollständig mandantenfähig ist, ist eine strikte Mandantenisolation entscheidend. Jede Abfrage muss auf die richtige Organisation beschränkt sein, und wir können uns nicht darauf verlassen, dass Entwickelnde daran denken, den richtigen Filter in jedem Codepfad hinzuzufügen.

Bevor Zen nativ IDOR-Schutz unterstützte, hatten wir eine kundenspezifische Implementierung, die die Mandantenbeschränkung auf Abfrageebene durchsetzte. Nachdem Zen erstklassigen Support für dieses Verhalten eingeführt hatte, migrierten wir von der Eigenentwicklung zur integrierten Funktionalität, was für uns deutlich weniger Code zur Wartung bedeutet.

Heute verifiziert Zen automatisch, dass Abfragen korrekt auf den aktuellen Mandanten beschränkt sind, selbst unter hoher Last. Nach der Einführung von Zens IDOR-Schutz stellten wir keine spürbaren Performance-Einbußen fest.

Erkennung & Prävention

Aikidos KI-Penetrationstests finden IDOR-Schwachstellen in Ihrer laufenden Anwendung, indem sie reale Angriffe simulieren. Zens IDOR-Schutz verhindert deren Einführung von vornherein, indem fehlende Mandantenfilter bereits während der Entwicklung erkannt werden.

Zusammen decken sie beide Seiten ab. KI-Penetrationstests validieren, dass Ihr bestehender Code sicher ist, und Zen stellt sicher, dass neuer Code sicher bleibt. Nutzen Sie KI-Penetrationstests, um bereits deployed Code zu auditieren. Nutzen Sie Zen, um Fehler beim Schreiben zu erkennen.

Erste Schritte

IDOR-Schutz ist heute in @aikidosec/firewall für Node.js verfügbar. Werfen Sie einen Blick in den Setup Guide, um loszulegen. Unterstützung für weitere Sprachen folgt in Kürze!

Teilen:

https://www.aikido.dev/blog/zen-stops-idor-vulnerabilities

Abonnieren Sie Bedrohungs-News.

4.7/5
Falschpositive Ergebnisse leid?

Probieren Sie Aikido, wie 100.000 andere.
Jetzt starten
Erhalten Sie eine personalisierte Führung

Von über 100.000 Teams vertraut

Jetzt buchen
Scannen Sie Ihre App nach IDORs und realen Angriffspfaden

Von über 100.000 Teams vertraut

Scan starten
Erfahren Sie, wie KI-Penetrationstests Ihre App testen

Von über 100.000 Teams vertraut

Testen starten

Sicherheit jetzt implementieren

Sichern Sie Ihren Code, Ihre Cloud und Ihre Laufzeit in einem zentralen System.
Finden und beheben Sie Schwachstellen schnell und automatisch.

Keine Kreditkarte erforderlich | Scan-Ergebnisse in 32 Sek.