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

TypWas es istBeispiel
HorizontalUser A bekommt Rechte von User B (gleiches Level)Alice greift auf Bobs Rechnungen zu
VertikalUser bekommt Rechte einer höheren RolleEditor 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:

JavaScript mass-assignment-vulnerable.js
// 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:

JSON mass-assignment-payload.json
{
  "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.

JavaScript allow-list-dto.js
// 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:

Plain parameter-pollution.txt
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 anderen

Wenn 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=user defensiv.
  • 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:

JavaScript verb-tampering-vulnerable.js
// 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 umgehen

Bei 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.

JavaScript force-browsing-defense.js
// 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:

JavaScript inconsistent-permission.js
// 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):

JavaScript toctou-race.js
// 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

/ Weiter

Zurück zu Autorisierung & Access Control

Zur Übersicht