Privilege-Escalation ist die Klasse, bei der ein eingeloggter User mehr Rechte erlangt, als er soll — entweder die eines anderen Users mit gleichem Permission-Level (horizontal) oder die eines höher-priviligierten Users (vertikal, klassisch „User wird Admin"). Die Vektoren sind vielfältig: vergessene Permission-Checks, Mass-Assignment auf Rollen-Felder, HTTP-Verb-Tampering, ungeprüfte Endpoint-Zugriffe. Dieser Artikel zeigt die Bug-Klassen und die Schutz-Patterns.
Horizontal vs. Vertikal
| Typ | Was es ist | Beispiel |
|---|---|---|
| Horizontal | User A bekommt Rechte von User B (gleiches Level) | Alice greift auf Bobs Rechnungen zu |
| Vertikal | User bekommt Rechte einer höheren Rolle | Editor wird Admin |
Horizontal ist im Kern dasselbe wie IDOR/BOLA — fehlender Resource-Owner-Check.
Vertikal ist die spektakulärere Variante: User wird zum Admin, kann dann Daten aller Tenants sehen oder Plattform-übergreifende Aktionen ausführen.
Mass-Assignment — der häufigste Vertikal-Vektor
Mass-Assignment ist, wenn ein Endpoint alle Felder aus dem Request-Body in einen DB-Record schreibt, ohne zu prüfen, welche Felder der User überhaupt setzen darf.
Anti-Pattern:
// Schadhaft
app.patch('/api/users/me', requireAuth, async (req, res) => {
await db.users.update({ id: req.user.id }, req.body);
res.json({ ok: true });
});Angreifer-Request:
{
"name": "Alice",
"email": "alice@example.com",
"role": "admin",
"isVerified": true,
"tenantId": "global"
}Der Code schreibt alle Felder, inklusive role: "admin". Privilege-Escalation in einem Request.
Schutz: Allow-list-DTOs.
// Schema-Validation mit Zod
import { z } from 'zod';
const UpdateProfileSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
// KEIN role, KEIN isVerified, KEIN tenantId
}).strict(); // strict: lehnt unbekannte Felder ab
app.patch('/api/users/me', requireAuth, async (req, res) => {
const parsed = UpdateProfileSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json(parsed.error);
await db.users.update({ id: req.user.id }, parsed.data);
res.json({ ok: true });
});Mit .strict() lehnt Zod jedes unerwartete Feld ab. Ohne .strict() werden zusätzliche Felder zwar nicht in parsed.data aufgenommen, aber .strict() ist defensiver — Angreifer-Versuche werden als 400-Error zurückgewiesen statt still ignoriert.
Pendant in anderen Sprachen:
- Python/FastAPI: Pydantic-Modelle mit
model_config = ConfigDict(extra='forbid'). - Java/Spring: DTOs mit
@JsonIgnoreProperties(ignoreUnknown = false). - Rails:
params.permit(:name, :email)(Strong Parameters).
Parameter-Pollution
HTTP Parameter Pollution (HPP) ist eine verwandte Klasse: derselbe Parameter taucht mehrfach im Request auf, und unterschiedliche Stack-Komponenten interpretieren das unterschiedlich.
Beispiel:
POST /api/users
Body: role=user&role=admin
Verschiedene Frameworks interpretieren das unterschiedlich:
- PHP: role = "admin" (last wins)
- Express (default): role = ["user", "admin"] (Array)
- Java Servlet: role = "user" (first wins)
- Validators-Pipeline: prüft nur einen Wert, schreibt einen anderenWenn ein Validator gegen den ersten Wert prüft und die DB-Library den zweiten schreibt — Privilege-Escalation.
Schutz:
- Body-Validierung mit strict-Schema (Allow-list).
- Query-Parameter-Validierung mit
?role=admin&role=userdefensiv. - WAF-Regeln für Parameter-Doppelung (Cloudflare, ModSecurity haben Regeln).
HTTP-Verb-Tampering
Manche Apps prüfen Permissions nur auf bestimmten HTTP-Methoden, vergessen aber andere.
Anti-Pattern:
// POST braucht admin
app.post('/api/users/:id/promote', requireAdmin, handler);
// GET, PUT, PATCH, DELETE auf gleicher URL?
// Wenn der Code default-mäßig alle Methods akzeptiert ohne Permission-Check —
// Angreifer kann mit PUT umgehenBei Frameworks mit „all methods" pro Route (manche PHP-/Java-Setups, alte Express-Patterns): unbekannte Methode wird vom selben Handler verarbeitet, Permission-Check fehlt für die Methode.
Schutz:
- Explizite Method-Wahl pro Route, nicht „accept all".
- 404 / 405 (Method Not Allowed) für nicht-erlaubte Verben.
- WAF-Regel für ungewöhnliche Methoden (
OPTIONS,HEAD,TRACE, Custom-Verbs).
Klassischer Fund: X-HTTP-Method-Override-Header. Manche Apps erlauben POST mit X-HTTP-Method-Override: DELETE als Workaround für Proxies, die nur POST/GET unterstützen. Wenn der Header ungeprüft durchläuft, kann ein POST zu einem DELETE werden — eventuell ohne dieselben Permission-Checks.
Force-Browsing
Force-Browsing ist der Versuch, durch direktes Eintippen einer URL Zugriff auf eine geschützte Ressource zu bekommen — ohne dass das UI dafür einen Link bietet.
Anti-Pattern: App zeigt Admin-Links nur für Admin-User. Backend prüft aber bei /admin/users-Endpoint keine Permission. Jeder User, der die URL kennt, kann den Endpoint nutzen.
Schutz: Default-Deny + Permission-Check auf jedem Endpoint, unabhängig davon, ob im UI sichtbar oder nicht.
// Globaler Default-Deny: alle Endpoints brauchen Auth + Permission
app.use((req, res, next) => {
if (PUBLIC_PATHS.includes(req.path)) return next();
if (!req.user) return res.status(401).end();
next();
});
// Admin-Routen mit explizitem Check
app.use('/admin/*', requireRole('admin'));
// Endpoints
app.get('/admin/users', handler);
app.get('/admin/billing', handler);
// requireRole-Middleware blockt unbefugten Zugriff für alle /admin/*Hidden Admin-Endpoints
Eng verwandt: Endpoints, die als „intern" oder „nur Admin" gedacht sind, aber öffentlich erreichbar.
Klassische Funde:
/api/debug/...— Debug-Endpoints in Produktion./health— gibt zu viel Internas zurück./admin/system-info,/admin/run-migration— interne Tooling-Endpoints./swagger,/api-docs— OpenAPI-Specs öffentlich./.git/,/.env— Source/Config in Web-Root.
Schutz:
- Interne Endpoints nicht ins Public-Internet routen — separate Service-Bind-Adresse oder Reverse-Proxy-Filter.
- Auth + Admin-Permission für jeden internen Endpoint, auch im Dev.
- Security-Scanner (Nuclei, dirsearch) als CI-Step laufen lassen — findet ungeschützte Endpoints automatisch.
Inkonsistente Permission-Logik zwischen Endpoints
Wenn dieselbe Aktion über zwei Endpoints erreichbar ist und nur einer Permission prüft, ist der andere ein Bypass.
Beispiel:
// GET /api/users/me — eingeloggter User
app.get('/api/users/me', requireAuth, (req, res) => {
res.json(req.user);
});
// GET /api/users/:id — könnte sensitiv sein
app.get('/api/users/:id', requireAuth, async (req, res) => {
// Vergessener Permission-Check!
const user = await db.users.findOne({ id: req.params.id });
res.json(user); // gibt fremde User-Daten zurück
});Pattern: Endpoint-Inventur. Für jede Resource: welche Endpoints exponieren sie, welche Permissions hat jeder davon. Inkonsistenzen sind die Quellen.
Tool-Unterstützung:
- OpenAPI-Spec mit Permission-Annotations als Single Source of Truth.
- OpenAPI → Permission-Test-Generator — automatischer Test pro Endpoint mit unterschiedlichen User-Rollen.
Race-Conditions in Privilege-Checks
Manche Privilege-Escalation-Bugs liegen in Race-Conditions zwischen Permission-Check und Action.
Klassisches Beispiel (TOCTOU — Time of Check to Time of Use):
// Schadhaft
app.post('/api/promote-user', async (req, res) => {
const requester = await db.users.findOne({ id: req.session.userId });
if (requester.role !== 'admin') return res.status(403).end();
// Zwischen Check und Update kann der Requester degradiert werden
// (durch parallelen Request) — Check ist veraltet
await db.users.update({ id: req.body.targetUserId }, { role: 'admin' });
});In der Praxis selten ausnutzbar, aber existiert. Schutz:
- Atomare DB-Operations — Check und Update in einer Transaction.
- Lock auf den User-Record während der Operation.
- Permission-Check direkt vor jeder kritischen DB-Modifikation, nicht nur am Request-Anfang.
Häufige Stolperfallen
Mass-Assignment auf Standard-User-Felder
isAdmin, role, permissions, tenantId, verifiedAt, credits, balance — alle klassische Privilege-Escalation-Felder. Allow-list-DTOs sind die einzige zuverlässige Verteidigung. „Wir schicken die nicht im Frontend, also kann der User die nicht setzen" ist falsch — User kann jeden Request manipulieren.
Self-Service-Endpoints und Admin-Endpoints vermischen
PATCH /api/users/me für Self-Service vs. PATCH /api/users/:id für Admin-Edit — wenn beide den gleichen Update-Code teilen und der Self-Service-Pfad nicht die Admin-Felder filtert, kann ein User über /me seine eigene Rolle erhöhen. Strikte Trennung: Self-Service hat anderes DTO als Admin-Edit.
Permission-Check nur in Middleware, nicht in Service-Layer
Wenn Middleware Auth/AuthZ prüft, aber der Service-Layer (intern aufrufbar von Background-Jobs, anderen Endpoints) keine Checks hat, kann ein anderer Endpoint den Service-Layer aufrufen und umgeht die Middleware. Best-Practice: Service-Layer-Funktionen mit Explicit-Permission-Parameter, prüfen am Eingang.
Admin-Endpoints im selben Subdomain wie öffentliche API
api.example.com mit /admin/*-Endpoints ist verbreitet. Risiko: ein einziger Permission-Check-Bug exposed sofort Admin-Funktionalität. Trennung in admin.example.com mit eigener Auth-Schicht (Kollektion-VPN, eigene mTLS-Cert-Prüfung, IP-Allowlist) reduziert das Risiko erheblich.
GraphQL-Nested-Mutations und Mass-Assignment
GraphQL erlaubt verschachtelte Inputs. Wenn mutation updateUser(input: UserUpdateInput!) ein role-Feld im Schema akzeptiert (auch wenn das Frontend es nie nutzt), kann jede:r es setzen. Schema-Design: separate Input-Types für unterschiedliche Use-Cases (SelfUpdateInput vs. AdminUpdateInput).
Versteckte Felder, die der Server nie validiert
Manche Apps lesen ein Feld X-Acting-User-Header (für Impersonation) und vertrauen ihm ungeprüft. Tests via Header-Manipulation: bei jedem Endpoint experimentieren mit Standard-Auth-Bypass-Headern (X-Real-User, X-Original-User, X-Forwarded-User). Bug-Bounty-Klassiker.
"Demo-Mode" als Production-Backdoor
Apps haben oft einen Demo-Mode für Marketing-Demos, in dem Auth gelockert ist (Demo-User-Login ohne Passwort, automatisches Admin-Token). Wenn der Demo-Mode in Production aktivierbar ist (via Env-Var, Feature-Flag, URL-Parameter), ist das ein Bypass. Demo-Mode strikt auf separater Subdomain oder in CI-Only-Builds halten.
Weiterführende Ressourcen
Externe Quellen
- OWASP API Top 10 — BFLA (Broken Function Level Authorization)
- OWASP API Top 10 — BOPLA (Mass-Assignment)
- OWASP Mass-Assignment Cheat Sheet
- PortSwigger — Access Control Issues
- PortSwigger — HTTP Parameter Pollution
- Zod (TypeScript Schema Validation)
- Pydantic (Python)