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:
// 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/DELETEbetroffen. - Massen-Exfiltration bei sequenziellen IDs (siehe gleich).
Sequenzielle IDs als Multiplikator
Wenn IDs sequenziell sind (1001, 1002, 1003, ...), kann ein Angreifer massenhaft Resources iterieren:
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 StundeVerbreitete 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/ordersalle 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.
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:
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.
// 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:
// 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 ProfilSchutz: Niemals User-ID aus dem Body nehmen. Immer aus req.user.
2. Nested-Resource ohne Hierarchie-Check:
// /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 abrufenSchutz: Hierarchie-Check — Task muss zu Project gehören, Project zu Org, Org muss dem User zugänglich sein.
3. „Funktion-IDOR" — falsche Endpoint-Aufruf:
// /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-Bypass4. 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:
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
- OWASP API Top 10 — BOLA
- OWASP Authorization Cheat Sheet
- PortSwigger — Access Control Vulnerabilities
- Burp Autorize Plugin
- CISA — IDOR Advisories