IDOR (Insecure Direct Object Reference) und ihre API-spezifische Variante BOLA (Broken Object Level Authorization) sind seit Jahren auf Platz 1 der OWASP API Top 10 — und in fast jedem Pentest- und Bug-Bounty-Report zu finden. Das Muster ist immer das gleiche: ein Endpoint akzeptiert eine Resource-ID aus der URL/Body, aber prüft nicht, ob der einlogte User darf, auf diese Resource zuzugreifen. Dieser Artikel zeigt die Klasse, warum UUIDs kein Schutz sind, und welche Architektur-Patterns IDOR strukturell verhindern.

Was IDOR/BOLA ist

Beispiel:

JavaScript idor-vulnerable.js
// Schadhaft
app.get('/api/invoices/:id', requireAuth, async (req, res) => {
  const invoice = await db.invoices.findOne({ id: req.params.id });
  res.json(invoice);
});

User Alice ist eingeloggt. Sie geht zu /api/invoices/1001 und sieht ihre eigene Rechnung. Sie ändert die URL zu /api/invoices/1002 — und sieht die Rechnung von Bob. IDOR.

Warum es entsteht:

  • Auth ist geprüft, aber AuthZ nicht.
  • Der Code geht implizit davon aus, dass User nur eigene Resources abfragen.
  • Frontend zeigt nur eigene Resources an — Backend prüft das nicht.

Schaden:

  • Datenleck (PII, Rechnungen, Nachrichten, Profile).
  • Daten-Manipulation, wenn auch PUT/PATCH/DELETE betroffen.
  • Massen-Exfiltration bei sequenziellen IDs (siehe gleich).

Sequenzielle IDs als Multiplikator

Wenn IDs sequenziell sind (1001, 1002, 1003, ...), kann ein Angreifer massenhaft Resources iterieren:

Bash idor-bulk-extraction.sh
for id in $(seq 1 100000); do
  curl -H "Cookie: sessionId=..." https://app.example.com/api/invoices/$id \
    >> all-invoices.json
done
# 100.000 Rechnungen exfiltriert in einer Stunde

Verbreitete Vorfälle:

  • USPS Informed Visibility (2018) — IDOR auf API erlaubte Zugriff auf 60 Mio. Konto-Daten.
  • Parler (2021) — sequenzielle Post-IDs, alle Posts inklusive gelöschter exfiltrierbar.
  • First American Financial (2019) — IDOR auf Document-IDs, 885 Mio. Mortgage-Dokumente exposed.
  • Bug-Bounty-Plattformen regelmäßig fünf- bis sechsstellige Auszahlungen für IDOR.

Sind UUIDs ein Schutz?

Verbreiteter Mythos: „Wir nutzen UUIDs als IDs — IDOR ist kein Problem, weil die IDs nicht ratbar sind."

Realität: UUIDs sind kein Autorisierungs-Mechanismus. Sie erschweren das Brute-Force-Enumeration, aber das Authorisierungs-Loch bleibt.

Wann UUIDs trotzdem geleakt werden:

  • Referrer-Header in Mails, externen Links.
  • URL-Sharing durch User (per Mail, Slack, in Bookmarks).
  • Client-Side-Logs (Sentry, Datadog, LogRocket) — UUIDs landen in Tickets.
  • Browser-History und Cache.
  • Server-Access-Logs — bei Log-Aggregation in SaaS-Tools.
  • List-Endpoints — wenn /api/orders alle Order-UUIDs zurückgibt, sind sie nicht mehr geheim.
  • UUID-v1 (Zeit-basiert) ist sogar teils ratbar — vermeiden.

Konsens: UUIDs sind schöne Security-by-Obscurity. Sie sind okay als ID-Format (besser als sequenziell), aber kein Ersatz für AuthZ-Check.

Resource-Owner-Check — die strukturelle Lösung

Pattern: bei jeder Resource-Operation prüfen, ob der User Zugriff hat.

JavaScript idor-secure-pattern.js
app.get('/api/invoices/:id', requireAuth, async (req, res) => {
  const invoice = await db.invoices.findOne({ id: req.params.id });

  // Resource existiert nicht
  if (!invoice) return res.status(404).end();

  // Resource-Owner-Check
  if (invoice.userId !== req.user.id && req.user.role !== 'admin') {
    return res.status(404).end();  // 404 statt 403 — verhindert Enumeration
  }

  res.json(invoice);
});

404 statt 403: Wenn der Server „403 Forbidden" sendet, bestätigt er „Resource existiert, du darfst nur nicht". Mit „404 Not Found" verrät der Server nichts über Existenz — Resource-Enumeration wird verhindert.

Noch besser — Query mit Owner-Filter:

JavaScript query-with-owner-filter.js
app.get('/api/invoices/:id', requireAuth, async (req, res) => {
  // Filter direkt in der Query — kann gar keine fremde Resource finden
  const invoice = await db.invoices.findOne({
    id: req.params.id,
    userId: req.user.id,
  });

  if (!invoice) return res.status(404).end();
  res.json(invoice);
});

Strukturell: die Query kann gar keine fremde Resource zurückgeben. Vergessen des Owner-Checks ist nicht mehr möglich — die DB-Anfrage hat den Filter eingebaut.

Tenant-Scoping als IDOR-Schutz

In Multi-Tenant-Apps ist Tenant-Scoping der häufigste IDOR-Schutz: jede Query bekommt automatisch den Tenant des aktuellen Users.

JavaScript tenant-scoping-pattern.js
// Globale Middleware: tenant aus User-Session setzen
app.use((req, res, next) => {
  req.db = wrapDb(db, { tenant: req.user.tenant });
  next();
});

// wrapDb fügt tenant automatisch in jede Query ein
function wrapDb(db, ctx) {
  return new Proxy(db, {
    get(target, prop) {
      if (prop === 'invoices') {
        return {
          findOne: q => target.invoices.findOne({ ...q, tenant: ctx.tenant }),
          find: q => target.invoices.find({ ...q, tenant: ctx.tenant }),
          // ... weitere Methoden
        };
      }
      return target[prop];
    }
  });
}

Damit kann ein Endpoint, der req.db.invoices.findOne({ id: ... }) aufruft, niemals eine Resource aus einem fremden Tenant zurückbekommen.

Vertieft in multi-tenancy-isolation.

Schwerer-zu-findende IDOR-Klassen

1. PUT/PATCH mit fremder Resource-ID im Body:

JavaScript idor-via-body.js
// Schadhaft
app.patch('/api/profile', requireAuth, async (req, res) => {
  const { userId, name, email } = req.body;
  await db.users.update({ id: userId }, { name, email });
  res.json({ ok: true });
});
// Angreifer: schickt userId=42 (Bob), ändert Bobs Profil

Schutz: Niemals User-ID aus dem Body nehmen. Immer aus req.user.

2. Nested-Resource ohne Hierarchie-Check:

JavaScript idor-nested.js
// /api/orgs/:orgId/projects/:projectId/tasks/:taskId
// Schadhaft: nur taskId wird genutzt
app.get('/api/orgs/:orgId/projects/:projectId/tasks/:taskId', async (req, res) => {
  const task = await db.tasks.findOne({ id: req.params.taskId });
  res.json(task);
});
// Angreifer kann taskId aus fremder Org abrufen

Schutz: Hierarchie-Check — Task muss zu Project gehören, Project zu Org, Org muss dem User zugänglich sein.

3. „Funktion-IDOR" — falsche Endpoint-Aufruf:

JavaScript idor-function.js
// /api/admin/users/:id existiert für Admins
// /api/user/:id existiert für normale User mit Self-Read
// Schadhaft: User ruft Admin-Endpoint und gibt fremde ID an
app.get('/api/admin/users/:id', requireAdmin, ...);
// Wenn requireAdmin fehlt oder umgangen wird — IDOR plus Admin-Permission-Bypass

4. GraphQL-IDOR via Direct Query:

GraphQL erlaubt direkte Resource-Lookups (query { user(id: 42) { ... } }). Wenn der Resolver keinen Owner-Check macht — IDOR. Vertieft in Kap 18 graphql-sicherheit.

OWASP API Top 10 — BOLA als #1

Die OWASP API Security Top 10 (2023) führen BOLA (Broken Object Level Authorization) als #1. Die Schätzung: 50 %+ aller API-Schwachstellen sind BOLA-Varianten.

Definition aus OWASP:

„APIs tend to expose endpoints that handle object identifiers, creating a wide attack surface of Object Level Access Control issues."

Praktische Konsequenz: Bei jeder API mit Resource-IDs in der URL/Body — systematischen Test auf BOLA durchführen. Bug-Bounty-Hunter wissen das; Pentests fangen damit an.

Test-Strategien

Manuell:

  • Endpoint pro Resource-Typ identifizieren.
  • Mit User-A einloggen, eigene Resource-IDs notieren.
  • Mit User-B einloggen, IDs von User-A in URLs/Bodies einsetzen.
  • Erwarteter Response: 404 (oder 403 wenn explizit gewollt).

Automatisiert:

  • IDOR-AutoBruteForce und ähnliche Tools.
  • Burp Suite mit „Autorize"-Plugin — schickt jeden Request zusätzlich mit anderen User-Sessions, vergleicht Responses.
  • Custom-Test-Suite mit zwei User-Accounts, kompletter Endpoint-Matrix.

Im CI:

JavaScript idor-integration-test.js
test('cross-user IDOR for /api/invoices/:id', async () => {
  // User A erstellt eine Rechnung
  const aliceSession = await login('alice@test.com');
  const { id: invoiceId } = await api.post('/api/invoices', { /* ... */ }, aliceSession);

  // User B versucht zuzugreifen
  const bobSession = await login('bob@test.com');
  const response = await api.get(`/api/invoices/${invoiceId}`, bobSession);

  expect(response.status).toBe(404);
});

Pro Endpoint mit Resource-IDs einen solchen Test. Skaliert mit Resource-Anzahl, ist aber die robusteste Garantie.

Häufige Stolperfallen

404 statt 403 als Existenz-Schutz

Wenn der Server „403 Forbidden" sendet, weiß der/die Angreifer:in: „Resource existiert, ich darf nur nicht". Mit „404 Not Found" gibt es keinen Unterschied zwischen „nicht existent" und „nicht erlaubt". Außer beim Owner: für eigene Resources „404" und „403" können unterschiedlich sein, weil der User sie kennt. Für fremde Resources: immer „404".

Bulk-Endpoints brauchen Bulk-Permission-Check

POST /api/invoices/batch-delete { ids: [1,2,3,4,5] } — alle IDs gehören dem User? Pro ID einzeln Resource-Owner-Check. Verbreiteter Bug: Endpoint prüft nur, ob der User Permission zum „delete" hat, nicht ob jede ID ihm gehört.

Filter-Parameter als IDOR-Vektor

GET /api/orders?userId=42 — wenn der Endpoint den userId-Filter ungeprüft übernimmt, kann jeder User Orders von User 42 abfragen. Filter-Parameter immer gegen User-Kontext validieren oder ignorieren.

Hidden Fields im Frontend als Schein-Schutz

Wenn das Frontend ein versteckes Field userId=42 sendet und das Backend das übernimmt — Angreifer:in modifiziert das Field. Hidden Fields sind kein Sicherheits-Mechanismus, sie sind eine UX-Konvention. Backend muss User-Kontext aus Session/Token nehmen, nie aus Client-Daten.

Cache-Layer übergeht Permission-Check

Wenn vor dem Permission-Check ein Cache liegt (Redis, CDN), kann derselbe Cache-Key für unterschiedliche User unterschiedliche Berechtigungen haben — aber Cache liefert die gleichen Daten. Cache-Key muss User-Kontext enthalten (z. B. invoice:{invoiceId}:{userId}) oder Cache nach Permission-Check.

Audit-Log mit Resource-Owner-Vergleich

Audit-Log sollte pro Access-Event festhalten: User-ID, Resource-ID, Resource-Owner-ID. Bei späterem Audit ist offensichtlich, wenn ein User auf Resources zugreift, die ihm nicht gehören. SIEM-Regeln können solche Pattern detektieren.

Mass-Assignment + IDOR als Kombi-Vektor

Wenn ein Endpoint PATCH /api/users/:id nicht nur den User-ID-Check umgeht (IDOR), sondern auch noch ein role=admin-Feld im Body akzeptiert (Mass-Assignment) — Privilege-Escalation in einem Request. Beide Klassen müssen getrennt geschützt werden. Vertieft in privilege-escalation.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Autorisierung & Access Control

Zur Übersicht