Allowlist statt Blocklist bei automatischem Daten-Binding
Automatisches Binden von Request-Daten an Entities oder Models ist bequem, kann aber dazu führen, dass Angreifer Felder wie is_admin einfach mitschicken und überschreiben. Wir zeigen den Unterschied zwischen Allowlist und Blocklist und ein konkretes Symfony-Beispiel mit DTOs statt direktem Entity-Binding.
Inhaltsverzeichnis
- 1. Was ist Mass Assignment?
- 2. Wie ein Mass-Assignment-Angriff funktioniert
- 3. Allowlist statt Blocklist
- 4. Typische Schwachstellen in der Praxis
- 5. DTOs statt direktem Entity-Binding in Symfony
- 6. Serializer-Gruppen als Alternative
- 7. Mass Assignment gezielt testen
- 8. Häufige Fehler bei der Umsetzung
- 9. Best Practices und Checkliste
- 10. Zusammenfassung
- 11. FAQ
1. Was ist Mass Assignment?
Mass Assignment beschreibt eine Schwachstelle, bei der eingehende Request-Daten automatisch und ungefiltert auf die Eigenschaften eines Entity- oder Model-Objekts gemappt werden. Frameworks bieten diese Automatik oft an, um Boilerplate-Code zu sparen: Ein JSON-Body wird direkt in ein PHP-Objekt oder Array-Update übersetzt, ohne dass jedes Feld einzeln zugewiesen werden muss. Diese Bequemlichkeit ist grundsätzlich sinnvoll, wird aber zum Sicherheitsrisiko, sobald niemand mehr bewusst entscheidet, welche Felder tatsächlich vom Client kommen dürfen.
Das Problem entsteht, sobald ein Entity mehr Felder besitzt, als der Client eigentlich setzen dürfen sollte. Enthält ein User-Entity neben Name und E-Mail auch ein Feld isAdmin oder role, kann ein Angreifer dieses Feld einfach im Request mitschicken. Wird es automatisch gebunden, hat sich der Angreifer selbst Admin-Rechte verschafft, ohne dass dafür eine explizite Sicherheitslücke im klassischen Sinn nötig war.
2. Wie ein Mass-Assignment-Angriff funktioniert
Der Ablauf ist denkbar einfach: Ein Angreifer registriert einen normalen Account und beobachtet, welche Felder beim Update des eigenen Profils akzeptiert werden. Oft reicht ein Blick in die Frontend-Quelltexte, API-Dokumentation oder in öffentliche Entity-Klassen eines Open-Source-Projekts, um interne Feldnamen wie isAdmin, role, balance oder verified zu erraten.
Das folgende Beispiel zeigt einen Symfony-Controller, der Profildaten aktualisiert. Die verwundbare Variante bindet den kompletten Request direkt an das Entity. Die abgesicherte Variante nutzt ein DTO mit einer festen Liste erlaubter Felder.
<?php
declare(strict_types=1);
namespace App\Controller\Api;
use App\Dto\UpdateProfileRequest;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;
final class ProfileApiController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly SerializerInterface $serializer,
) {
}
// Verwundbar: Request-JSON wird direkt auf das Entity gemappt
#[Route('/api/profile', methods: ['PATCH'])]
public function updateVulnerable(Request $request): JsonResponse
{
$user = $this->getUser();
$data = json_decode($request->getContent(), true);
// Jedes Feld aus dem Request landet ungeprüft auf dem Entity,
// inklusive isAdmin oder role, falls der Client sie mitschickt.
foreach ($data as $property => $value) {
$setter = 'set' . ucfirst($property);
if (method_exists($user, $setter)) {
$user->$setter($value);
}
}
$this->entityManager->flush();
return $this->json(['status' => 'updated']);
}
// Abgesichert: DTO mit fester Allowlist an erlaubten Feldern
#[Route('/api/profile', methods: ['PATCH'])]
public function updateSecured(Request $request): JsonResponse
{
/** @var UpdateProfileRequest $dto */
$dto = $this->serializer->deserialize(
$request->getContent(),
UpdateProfileRequest::class,
'json'
);
$user = $this->getUser();
// Nur Felder, die im DTO explizit deklariert sind, können überhaupt ankommen
$user->setDisplayName($dto->displayName);
$user->setEmail($dto->email);
$this->entityManager->flush();
return $this->json(['status' => 'updated']);
}
}
3. Allowlist statt Blocklist
Ein Blocklist-Ansatz versucht, gefährliche Felder explizit auszuschließen, zum Beispiel indem isAdmin vor dem Mapping aus dem Array entfernt wird. Dieser Ansatz ist fehleranfällig, weil jedes neue sensible Feld, das dem Entity später hinzugefügt wird, aktiv in die Blockliste aufgenommen werden muss. Vergisst ein Entwickler diesen Schritt, ist die Lücke sofort wieder da, ohne dass es beim Review unbedingt auffällt.
Ein Allowlist-Ansatz dreht das Prinzip um: Es wird explizit definiert, welche Felder ein Client setzen darf, alles andere ist standardmäßig gesperrt. Neue Felder sind damit automatisch sicher, bis sie bewusst zur Allowlist hinzugefügt werden. Genau dieses Prinzip setzen DTOs, Symfony-Forms mit expliziter Feldliste oder Serializer-Gruppen technisch um.
4. Typische Schwachstellen in der Praxis
Neben klassischen User-Entities mit Rollenfeldern sind auch Bestellungen mit Preis- oder Statusfeldern gefährdet: Ein Client, der beim Checkout ein price-Feld mitschicken kann, das direkt übernommen wird, kann theoretisch den Preis selbst bestimmen. Aehnlich kritisch sind Statusfelder wie isPaid oder isApproved, die ausschließlich serverseitig durch Geschäftslogik gesetzt werden sollten. Auch Zuordnungsfelder wie userId, tenantId oder organizationId sind gefährdet, weil ein Angreifer damit theoretisch Datensätze einem fremden Konto zuordnen oder aus einem fremden Mandanten lesen könnte, wenn diese Felder ungeprüft aus dem Request übernommen werden.
Auch Doctrine-Formulare mit CSRF-Schutz sind nicht automatisch sicher, wenn das zugrunde liegende FormType alle Entity-Felder unreflektiert einbindet, statt bewusst nur die tatsächlich vom Client zu setzenden Felder zu deklarieren. Ein FormType ist ein guter Startpunkt für eine Allowlist, muss aber ebenso sorgfältig gepflegt werden wie ein DTO. Besonders bei generierten CRUD-Oberflächen, die automatisch aus dem Entity abgeleitet werden, schleicht sich diese Nachlässigkeit gerne ein, weil der Komfort der automatischen Generierung gegen die Sicherheit der expliziten Feldliste abgewogen werden muss.
5. DTOs statt direktem Entity-Binding in Symfony
In Symfony lassen sich DTOs elegant mit der Serializer-Komponente und Property-Validierung über den Symfony Validator kombinieren. Ein DTO enthält ausschließlich die Felder, die für einen bestimmten Anwendungsfall wirklich benötigt werden, inklusive eigener Validierungsregeln, unabhängig von den Constraints des Entitys.
Der Mapping-Schritt vom DTO auf das Entity erfolgt anschließend explizit im Controller oder in einem dedizierten Mapper-Service, mit klar benannten Settern für jedes erlaubte Feld. Dieser zusätzliche Schritt wirkt auf den ersten Blick wie mehr Code, reduziert aber die Angriffsfläche erheblich und macht die erlaubten Felder für jeden Entwickler auf einen Blick sichtbar.
6. Serializer-Gruppen als Alternative
Wer aus Aufwandsgründen keine eigenen DTOs für jeden Endpunkt anlegen möchte, kann in Symfony mit Serializer-Gruppen arbeiten. Dabei werden Entity-Properties mit Gruppen wie profile:write annotiert, und nur Felder dieser Gruppe werden beim Deserialisieren berücksichtigt. Sensible Felder wie roles bekommen bewusst keine Write-Gruppe zugewiesen.
Diese Lösung ist schneller umzusetzen als vollständige DTOs, birgt aber das Risiko, dass ein neues Feld versehentlich mit einer zu weiten Gruppe versehen wird. DTOs bleiben deshalb die robustere Lösung für sicherheitskritische Entities wie User oder Payment. Eine sinnvolle Faustregel ist, Serializer-Gruppen für unkritische, intern genutzte Entities zu verwenden und für alles, was Rollen, Zahlungen oder Berechtigungen betrifft, konsequent auf DTOs zu setzen.
7. Mass Assignment gezielt testen
Ein einfacher, aber wirkungsvoller Test schickt bei jedem schreibenden Endpunkt zusätzlich zu den erwarteten Feldern ein bekanntes sensibles Feld wie isAdmin oder role mit einem abweichenden Wert mit. Nach dem Request wird geprüft, ob sich der Wert des sensiblen Feldes tatsächlich verändert hat. Bleibt er unverändert, funktioniert die Allowlist wie erwartet.
Dieser Test lässt sich gut automatisieren und als Pflichtbestandteil für jeden neuen PATCH- oder PUT-Endpunkt in die CI-Pipeline aufnehmen, ähnlich wie der Zwei-Nutzer-Test bei BOLA-Schwachstellen. Sinnvoll ist zudem, den Test mit mehreren sensiblen Feldern gleichzeitig laufen zu lassen, um auch Kombinationen aus mehreren, jeweils für sich harmlos wirkenden Feldern abzudecken.
8. Häufige Fehler bei der Umsetzung
Ein häufiger Fehler ist, DTOs oder FormTypes zwar für neue Endpunkte konsequent einzusetzen, bei bestehenden, älteren Endpunkten aber weiterhin direktes Entity-Binding zu verwenden, weil eine Migration als zu aufwendig empfunden wird. Genau diese Altlasten sind in der Praxis die häufigste Quelle für Mass-Assignment-Vorfälle, da neue Sicherheitsstandards oft nur für neuen Code gelten und bestehender Code unangetastet bleibt, bis ein konkreter Vorfall die Nachbesserung erzwingt.
Ein weiterer Fehler ist, Admin-Bereiche für sicherer zu halten als öffentliche APIs und dort auf eine Allowlist zu verzichten. Admin-Oberflächen werden aber ebenso über HTTP-Requests angesprochen und sind damit genauso anfällig, sobald ein Angreifer ein kompromittiertes oder falsch berechtigtes Konto nutzt. Auch interne Tools, die nur von Kollegen genutzt werden, sind kein Freibrief für direktes Entity-Binding, da ein kompromittierter Mitarbeiter-Account dieselben Folgen haben kann wie ein externer Angriff.
9. Best Practices und Checkliste
Jeder schreibende Endpunkt sollte mit einer expliziten Allowlist arbeiten, sei es über ein DTO, ein FormType mit definierten Feldern oder Serializer-Gruppen. Sensible Felder wie Rollen, Preise oder Statuswerte dürfen ausschließlich durch dedizierte, serverseitige Endpunkte oder interne Services gesetzt werden, niemals über denselben Endpunkt wie unkritische Profildaten. Diese Trennung sollte auch architektonisch sichtbar sein, etwa durch getrennte Controller oder Namensräume für administrative und selbstverwaltete Operationen.
Ergänzend hilft ein automatisierter Test pro Endpunkt, der versucht, ein bekanntes sensibles Feld zu überschreiben, sowie ein Code-Review-Standard, der direktes Entity-Binding aus Request-Daten grundsätzlich ablehnt.
| Feld | Risiko bei Mass Assignment | Blocklist-Ansatz | Allowlist-Ansatz |
|---|---|---|---|
| isAdmin / role | Selbst-Ernennung zum Administrator | Muss aktiv gesperrt werden | Existiert im DTO gar nicht |
| price | Client bestimmt Verkaufspreis selbst | Leicht vergessen | Nur serverseitig berechenbar |
| isPaid / isApproved | Status ohne Geschäftslogik gesetzt | Fehleranfällig bei neuen Feldern | Kein Setter im DTO vorhanden |
| createdAt / userId | Fremde Zuordnung von Datensätzen | Oft übersehen | Wird serverseitig gesetzt, nie vom Client |
Mironsoft
Security-Audits, OWASP-konforme Härtung und sichere Architektur
Anwendungen, die einem echten Angriffsversuch tatsächlich standhalten?
Wir prüfen bestehende Anwendungen auf klassische OWASP-Schwachstellen, unsichere Authentifizierung und fehlende Input-Validierung und bauen daraus eine Architektur, die Angriffsflächen strukturell reduziert statt nur einzelne Symptome zu flicken.
Security-Audit
OWASP Top 10, Auth-Flows und Input-Validierung systematisch auf Schwachstellen prüfen.
Sichere Architektur
Rate-Limiting, Verschlüsselung und Zugriffskontrollen von Grund auf richtig aufbauen.
Incident-Vorbereitung
Logging, Monitoring und Reaktionsprozesse für den Ernstfall etablieren.
10. Zusammenfassung
Mass Assignment
Ursache
Automatisches Binden aller Request-Felder an ein Entity.
Erkennung
Test mit bekanntem sensiblen Feld und abweichendem Wert.
Fix
DTOs oder Serializer-Gruppen als explizite Allowlist.
Prävention
Kein direktes Entity-Binding, konsequentes Code-Review.