Warum Allowlisting die einzig robuste Verteidigungslinie ist
Wer Benutzereingaben nur gegen bekannte Angriffsmuster prüft, übersieht zwangsläufig die nächste Variante. Dieser Artikel zeigt, warum Allowlist-Validierung jede Blacklist schlägt, wie PHPs filter_var Funktion richtig eingesetzt wird, welche Fallstricke Magentos Formularvalidierung bereithält und warum Validierung und Sanitization strikt getrennte Aufgaben mit unterschiedlichen Zielen sind.
Inhaltsverzeichnis
- 1. Warum Allowlists Blacklists schlagen
- 2. Die richtige Validierungsebene: serverseitig ist Pflicht
- 3. Typ, Format, Bereich und Länge: die vier Prüfdimensionen
- 4. PHP filter_var(): Filter, Flags und Fallstricke
- 5. Validierungsbibliotheken: Respect/Validation und Symfony Validator
- 6. Magento-Formularvalidierung: DataObject und Form-XML
- 7. Magento\Framework\Validator und EAV-Attributvalidierung
- 8. Kanonisierung vor der Validierung
- 9. Validierung vs. Sanitization: zwei unterschiedliche Aufgaben
- 10. Zusammenfassung
- 11. FAQ
1. Warum Allowlists Blacklists schlagen
Eine Blacklist (Denylist) zählt auf, was verboten ist: bestimmte Zeichen, bestimmte Wörter, bestimmte Muster. Das Grundproblem ist strukturell und lässt sich nicht durch mehr Sorgfalt beheben: Eine Denylist kann nur Angriffsmuster blockieren, die zum Zeitpunkt ihrer Erstellung bekannt waren. Jede neue Umgehungstechnik, jede alternative Kodierung, jeder Case-Trick oder jedes Unicode-Homoglyph, das nicht explizit in der Liste steht, kommt ungehindert durch. Ein klassisches Beispiel: Ein Filter blockiert <script>, lässt aber <img src=x onerror=alert(1)> passieren, weil niemand an dieses Attribut gedacht hat.
Eine Allowlist (Whitelist) kehrt das Prinzip um: Sie definiert exakt, was erlaubt ist, und lehnt implizit alles andere ab. Das ist das Prinzip fail closed statt fail open. Ein Feld für Produkt-SKUs, das nur Großbuchstaben, Ziffern und Bindestriche zulässt, kann durch keine noch so kreative Umgehungstechnik unterlaufen werden, weil jede Eingabe, die nicht exakt dem erlaubten Muster entspricht, verworfen wird. Der Mehraufwand liegt im Design: Man muss vorab wissen, welche Werte gültig sind. Dieser Aufwand zahlt sich aus, weil eine Allowlist im Gegensatz zur Denylist nicht ständig gegen neue Angriffstechniken nachgepflegt werden muss.
2. Die richtige Validierungsebene: serverseitig ist Pflicht
Client-seitige Validierung mit HTML5-Attributen wie required, pattern oder maxlength und begleitendem JavaScript verbessert die Nutzererfahrung, weil Fehler sofort sichtbar werden, ohne einen Roundtrip zum Server. Sie ist aber niemals eine Sicherheitsgrenze. Jeder Request kann direkt per curl, über die Browser-DevTools oder mit einem Tool wie Burp Suite ohne jede clientseitige Prüfung an den Server gesendet werden. Wer sich auf ein pattern-Attribut im Formular verlässt, um SQL-Injection oder XSS zu verhindern, hat de facto keine Validierung.
Die Regel ist deshalb eindeutig: Jede Eingabe wird serverseitig validiert, unabhängig davon, ob eine clientseitige Prüfung existiert. Clientseitige Validierung ist reine UX-Convenience und darf redundant zur Server-Validierung existieren, niemals als Ersatz. Das gilt auch für Eingaben, die scheinbar aus vertrauenswürdigen Quellen stammen, etwa aus internen Admin-Formularen, REST-Endpunkten mit API-Key oder aus CSV-Importen. Jede Grenze zwischen Systemen, auch innerhalb derselben Codebasis zwischen Controller und Service-Layer, ist ein potenzieller Punkt, an dem unvalidierte Daten eindringen können, insbesondere wenn Services später wiederverwendet werden.
3. Typ, Format, Bereich und Länge: die vier Prüfdimensionen
Robuste Validierung prüft nicht nur, ob ein Wert "irgendwie plausibel" aussieht, sondern deckt vier Dimensionen gezielt ab. Typ: Ein numerisches Feld muss tatsächlich ein Integer oder Float sein, nicht ein String, der zufällig numerisch aussieht. PHPs schwache Typisierung macht das besonders wichtig, da "0e123" == "0e456" in loser Vergleichslogik als true ausgewertet wird. Format: Ein Muster wie eine E-Mail-Adresse, eine IBAN oder eine Produkt-SKU folgt einer festen Struktur, die per anchored regex geprüft wird, also mit ^ am Anfang und $ am Ende, damit kein Teilstring genügt.
Bereich: Numerische Werte brauchen explizite Ober- und Untergrenzen, etwa eine Bestellmenge zwischen 1 und 999 oder ein Rabatt-Prozentsatz zwischen 0 und 100. Ohne Bereichsprüfung akzeptiert ein Formular auch eine Menge von minus 5 oder zehn Millionen. Länge: Strings brauchen Minimal- und Maximalgrenzen, sowohl aus fachlicher Sicht, etwa ein Firmenname mit maximal 255 Zeichen für die Datenbankspalte, als auch aus Sicherheitssicht, um Denial-of-Service durch übergroße Payloads oder ReDoS-anfällige Regex-Auswertungen zu verhindern. Alle vier Dimensionen gehören in jede nicht-triviale Validierungsroutine.
<?php
declare(strict_types=1);
namespace Mironsoft\SeoSuite\Validator;
use InvalidArgumentException;
/**
* Strict allowlist validator for URL slugs.
* Rejects everything that is not explicitly permitted.
*/
final class SlugValidator
{
// Only lowercase letters, digits and hyphens, 1-80 chars, anchored on both ends
private const PATTERN = '/^[a-z0-9]+(?:-[a-z0-9]+)*$/';
private const MAX_LENGTH = 80;
/**
* Validates a slug against the allowlist pattern and length bounds.
*
* @param string $slug Raw input value.
* @return string The validated slug.
* @throws InvalidArgumentException If the slug is not allowed.
*/
public function validate(string $slug): string
{
if ($slug === '' || mb_strlen($slug) > self::MAX_LENGTH) {
throw new InvalidArgumentException('Slug length out of allowed range.');
}
// preg_match returns 1 only on a full anchored match, never a partial one
if (preg_match(self::PATTERN, $slug) !== 1) {
throw new InvalidArgumentException(sprintf('Slug "%s" is not on the allowlist.', $slug));
}
return $slug;
}
}
4. PHP filter_var(): Filter, Flags und Fallstricke
PHPs eingebaute Funktion filter_var() deckt die häufigsten Validierungsfälle ab, ohne eine externe Bibliothek zu benötigen. FILTER_VALIDATE_INT, FILTER_VALIDATE_FLOAT, FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_URL und FILTER_VALIDATE_IP geben bei gültiger Eingabe den (typisierten) Wert zurück, bei ungültiger Eingabe false. Genau hier liegt der häufigste Fallstrick: false mit lockerem Vergleich (==) statt striktem Vergleich (===) zu prüfen, führt zu Fehlern, weil 0, ein Integer-Rückgabewert bei gültiger Nulleingabe, in loser Logik ebenfalls als "falsy" behandelt wird.
Ein weiterer Fallstrick: FILTER_VALIDATE_EMAIL prüft ausschließlich Syntax nach RFC 822 und verwandten Standards, niemals ob die Domain existiert oder einen MX-Record hat. Für echte Zustellbarkeit ist zusätzlich eine DNS-Prüfung oder ein Double-Opt-In nötig. Die Optionen min_range und max_range bei FILTER_VALIDATE_INT ermöglichen Bereichsprüfung direkt im Filter-Aufruf, ohne zusätzlichen Code. Wichtig ist außerdem, FILTER_VALIDATE_* nicht mit FILTER_SANITIZE_* zu verwechseln: Sanitize-Filter transformieren stillschweigend, ohne bei ungültiger Eingabe abzulehnen, und sind deshalb kein Ersatz für echte Validierung.
<?php
declare(strict_types=1);
// Common filter_var() pitfalls and correct usage
// PITFALL: FILTER_VALIDATE_INT returns false on failure, but false == 0 in loose comparisons
$rawId = '0';
$productId = filter_var($rawId, FILTER_VALIDATE_INT);
if ($productId === false) { // must use strict comparison
throw new InvalidArgumentException('Invalid product id.');
}
// Bounded integer validation with min_range / max_range options
$page = filter_var($_GET['page'] ?? '1', FILTER_VALIDATE_INT, [
'options' => ['default' => 1, 'min_range' => 1, 'max_range' => 500],
]);
// PITFALL: FILTER_VALIDATE_EMAIL accepts syntactically valid but non-deliverable addresses
// and does NOT verify the domain has an MX record - treat it as a syntax check only
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
if ($email === false) {
throw new InvalidArgumentException('Invalid email format.');
}
// FILTER_VALIDATE_FLOAT with decimal option for locale-sensitive input
$price = filter_var('19,99', FILTER_VALIDATE_FLOAT, [
'options' => ['decimal' => ','],
]);
// PITFALL: filter_var() with FILTER_SANITIZE_* silently transforms instead of rejecting
// Do not use sanitize filters where validation (rejection) is required
$unsafe = filter_var($_GET['comment'] ?? '', FILTER_SANITIZE_SPECIAL_CHARS); // wrong tool for validation
5. Validierungsbibliotheken: Respect/Validation und Symfony Validator
Für komplexere Regeln als einzelne filter_var()-Aufrufe lohnt sich eine dedizierte Validierungsbibliothek. Respect/Validation bietet eine fluente, verkettbare API, bei der jede Regel als eigener Validator komponiert wird, etwa v::stringType()->length(3, 32)->regex($pattern). Die Bibliothek wirft bei assert() eine ValidationException mit einer Liste aller verletzten Regeln, was präzises Feedback für API-Antworten oder Formularfehler liefert, ohne dass man selbst eine Fehlersammel-Logik schreiben muss.
Symfony Validator verfolgt einen deklarativen Ansatz über Constraint-Objekte, die unabhängig vom restlichen Symfony-Framework in jedem PHP-Projekt einsetzbar sind, auch in Magento-Modulen. Constraints wie Assert\NotBlank, Assert\Length und Assert\Regex werden zu einer Liste kombiniert und gegen einen Wert geprüft; das Ergebnis ist eine ConstraintViolationList. Beide Bibliotheken folgen konsequent dem Allowlist-Prinzip: Man definiert positiv, was ein gültiger Wert ausmacht, statt Ausschlusslisten für ungültige Werte zu pflegen. Der Vorteil gegenüber Handrolled-Validatoren liegt in getesteten Edge-Cases, etwa bei Unicode-Normalisierung oder Multibyte-Längenberechnung.
<?php
declare(strict_types=1);
use Respect\Validation\Validator as v;
use Respect\Validation\Exceptions\ValidationException;
// Respect/Validation: declarative allowlist chain, collects all failed rules
$customerAgeValidator = v::intType()->between(18, 120);
try {
$customerAgeValidator->assert($input['age']);
} catch (ValidationException $e) {
// $e->getMessages() returns one message per failed rule
throw new InvalidArgumentException(implode(' ', $e->getMessages()));
}
// Chained allowlist validator for a SKU field
$skuValidator = v::stringType()
->length(3, 32)
->regex('/^[A-Z0-9\-]+$/');
if (!$skuValidator->validate($input['sku'])) {
throw new InvalidArgumentException('SKU does not match the allowed pattern.');
}
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;
// Symfony Validator: constraint chain equivalent to the SKU rule above
$validator = Validation::createValidator();
$violations = $validator->validate($input['sku'], [
new Assert\NotBlank(),
new Assert\Length(min: 3, max: 32),
new Assert\Regex(pattern: '/^[A-Z0-9\-]+$/'),
]);
if (count($violations) > 0) {
throw new InvalidArgumentException((string) $violations);
}
6. Magento-Formularvalidierung: DataObject und Form-XML
Magento definiert Validierungsregeln für UI-Component-Formulare deklarativ in der Form-XML unter <validation>, mit Regeln wie required-entry, validate-length, min_text_length, max_text_length oder validate-digits. Diese Regeln werden clientseitig über die jQuery-Validation-Integration von Magento ausgewertet und liefern sofortiges Feedback im Admin-Grid oder in Storefront-Formularen. Entscheidend ist: Diese XML-Regeln laufen im Browser und sind exakt die Art von Convenience-Validierung, die niemals als alleinige Absicherung dienen darf. Jeder Controller, jedes Repository und jeder Service, der die übermittelten Daten entgegennimmt, muss sie serverseitig erneut prüfen.
Magento\Framework\DataObject selbst validiert nicht automatisch, es ist ein reiner Datencontainer mit magischen Gettern und Settern. Validierung muss explizit in der Save-Logik, in einem Repository oder in einem dedizierten Validator-Service implementiert werden, bevor Daten in die Datenbank geschrieben werden. Ein verbreiteter Fehler ist, Formulardaten direkt aus getRequest()->getParams() in ein DataObject zu laden und ungeprüft an ein Repository weiterzureichen, das selbst wiederum keine eigene Validierung durchführt.
<?xml version="1.0"?>
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<fieldset name="general">
<field name="sku" formElement="input">
<settings>
<dataType>text</dataType>
<label translate="true">SKU</label>
<!-- Client-side rendered validation rules, mirrored by JS UI but never a substitute for server checks -->
<validation>
<rule name="required-entry" xsi:type="boolean">true</rule>
<rule name="validate-length" xsi:type="boolean">true</rule>
<rule name="min_text_length" xsi:type="number">3</rule>
<rule name="max_text_length" xsi:type="number">32</rule>
<rule name="validate-code" xsi:type="boolean">true</rule>
</validation>
</settings>
</field>
<field name="qty" formElement="input">
<settings>
<dataType>number</dataType>
<validation>
<rule name="required-entry" xsi:type="boolean">true</rule>
<rule name="validate-zero-or-greater" xsi:type="boolean">true</rule>
<rule name="validate-digits" xsi:type="boolean">true</rule>
</validation>
</settings>
</field>
</fieldset>
</form>
7. Magento\Framework\Validator und EAV-Attributvalidierung
Magento\Framework\Validator stellt eine ValidatorChain bereit, mit der mehrere Regeln zu einer wiederverwendbaren, serverseitigen Prüfkette kombiniert werden, etwa Magento\Framework\Validator\StringLength für Längenprüfung oder Magento\Framework\Validator\Regex für Formatprüfung. Anders als die Form-XML-Regeln läuft diese Kette im PHP-Code des Servers und kann deshalb tatsächlich als Sicherheitsgrenze dienen. Ein Validator-Service, der die Kette kapselt, lässt sich per Dependency Injection in Repositories, Save-Controller und Import-Handler einbinden und stellt einheitliche Regeln unabhängig vom Eingangskanal sicher.
EAV-Attributvalidierung wird deklarativ über input_filter und validate_rules auf dem Attribut konfiguriert, etwa als alphanum-with-spaces oder mit einer eigenen Regex im Backend-Formular des Attributs. Diese Regeln greifen jedoch nur, wenn der Speicherpfad tatsächlich über das EAV-Attributmodell läuft, also über Magento\Eav\Model\Attribute und die zugehörigen Backend-Modelle. Direkte SQL-Writes, Massenimporte über Magento\ImportExport mit eigener Verarbeitung oder REST-Endpunkte, die Repositories umgehen, laufen an dieser Validierung vorbei und benötigen eine eigene, explizite Prüfung.
<?php
declare(strict_types=1);
namespace Mironsoft\SeoSuite\Model;
use Magento\Framework\Validator\ValidatorChain;
use Magento\Framework\Validator\StringLength;
use Magento\Framework\Validator\Regex;
use Magento\Framework\Validator\Exception as ValidatorException;
/**
* Server-side validation using Magento's Framework\Validator chain.
* Runs independently of any EAV attribute validation configured in the admin.
*/
final class MetaTitleValidator
{
/**
* Builds and runs the allowlist validation chain for a meta title.
*
* @param string $value Raw meta title input.
* @return void
* @throws ValidatorException If any rule in the chain fails.
*/
public function validate(string $value): void
{
$chain = new ValidatorChain();
$chain->addValidator(new StringLength(['min' => 1, 'max' => 70]));
// Anchored allowlist: letters, digits, spaces and a small set of punctuation only
$chain->addValidator(new Regex(['pattern' => '/^[\p{L}\p{N}\s\-.,:!?]+$/u']));
if (!$chain->isValid($value)) {
throw new ValidatorException(implode(' ', $chain->getMessages()));
}
}
}
// EAV attribute validation is configured declaratively, not in PHP:
// bin/magento setup:upgrade after adding input_filter / validate_rules to an eav_attribute row,
// e.g. 'input_filter' => 'trim', 'validate_rules' => serialize(['input_validation' => 'alphanum-with-spaces'])
8. Kanonisierung vor der Validierung
Eine Validierungsregel prüft eine konkrete Bytefolge, keine "Bedeutung". Wenn dieselbe logische Eingabe in mehreren Kodierungen dargestellt werden kann, etwa durch URL-Encoding, doppeltes URL-Encoding, Unicode-Normalisierungsformen (NFC/NFD), UTF-8-Overlong-Encoding oder gemischte Groß- und Kleinschreibung bei Dateipfaden, kann eine Regex, die eine Form abdeckt, eine andere Form durchlassen. Genau das ist der Mechanismus hinter vielen Path-Traversal- und Filter-Bypass-Angriffen: %2e%2e%2f oder ..%c0%af umgehen einen Filter, der nur nach ../ sucht.
Die Konsequenz: Kanonisierung muss vor der Validierung stattfinden, nicht danach. Eingaben werden zuerst in eine einzige, definierte Normalform gebracht, etwa via urldecode() bis zur Fixpunkt-Stabilität, Normalizer::normalize($str, Normalizer::FORM_C) für Unicode-Text oder realpath() für Dateisystempfade, bevor die Allowlist-Prüfung greift. Wird stattdessen erst validiert und danach dekodiert, kann eine bösartige Payload die Prüfung in kodierter Form passieren und erst nach der Dekodierung ihre eigentliche, nicht erlaubte Form annehmen. Diese Reihenfolge, normalisieren vor prüfen statt umgekehrt, ist einer der am häufigsten übersehenen Punkte in Validierungsroutinen.
9. Validierung vs. Sanitization: zwei unterschiedliche Aufgaben
Validierung und Sanitization werden in der Praxis oft synonym verwendet, sind aber grundverschiedene Operationen mit unterschiedlichen Garantien. Validierung ist eine binäre Entscheidung: Ein Wert entspricht der Allowlist oder er wird abgelehnt. Es gibt kein "teilweise gültig". Sanitization dagegen transformiert eine Eingabe, etwa durch Entfernen, Escapen oder Ersetzen bestimmter Zeichen, und liefert immer ein Ergebnis zurück, unabhängig davon, wie die ursprüngliche Eingabe aussah.
Das Konfliktpotenzial entsteht, wenn beide Konzepte vermischt werden: Ein Entwickler "validiert" eine Eingabe, indem er gefährliche Zeichen mit strip_tags() oder htmlspecialchars() entfernt, und geht danach fälschlich davon aus, die Eingabe sei nun geprüft und sicher für jeden Kontext. Sanitization für den HTML-Ausgabekontext macht eine Eingabe aber nicht automatisch gültig für eine SQL-Query, einen Dateipfad oder eine Geschäftsregel wie eine positive Bestellmenge. Die saubere Trennung lautet: Validierung entscheidet, ob eine Eingabe fachlich und strukturell akzeptiert wird, kontextspezifisches Escaping oder Encoding (Sanitization im engeren Sinn) erfolgt separat, unmittelbar vor der Verwendung im jeweiligen Ausgabekontext, niemals als Ersatz für die vorgelagerte Validierung.
| Aufgabe | Unsicher / Naiv | Empfohlene Strategie | Vorteil |
|---|---|---|---|
| E-Mail-Prüfung | Blacklist verbotener Zeichen manuell pflegen | filter_var($v, FILTER_VALIDATE_EMAIL) |
Getestetes, allowlist-basiertes Format |
| Sonderzeichen in Freitext | str_replace() bekannter Gefahrenstrings |
Erlaubtes Zeichenset per anchored Regex | Kein Wettlauf gegen neue Bypässe |
| Validierungsort | Nur clientseitiges HTML5/JS-Pattern | Serverseitige Pflichtprüfung, Client nur UX | Nicht per curl umgehbar |
| Typprüfung | Lose Vergleiche (==, is_numeric) |
Strikte Typisierung + FILTER_VALIDATE_INT |
Keine Type-Juggling-Fallen |
| Datei-Upload | Endungs-Blacklist (.php, .phtml) |
Allowlist erlaubter MIME-Types + Inhaltsprüfung | Keine übersehenen Ausführungsendungen |
Mironsoft
Security-Audits, Code-Reviews und sichere Magento-Entwicklung
Eingabevalidierung professionell absichern?
Wir prüfen eure Magento- und PHP-Codebasis auf Blacklist-Fallstricke, fehlende serverseitige Validierung und unsaubere Trennung von Validierung und Sanitization, und implementieren robuste Allowlist-Strategien für Formulare, APIs und Import-Schnittstellen.
Security-Code-Review
Systematische Prüfung aller Eingabepunkte auf Allowlist-Konformität und OWASP-Standards
Validator-Refactoring
Blacklist-Logik durch Respect/Validation, Symfony Validator oder Framework\Validator ersetzen
Formular- & API-Härtung
Magento Form-XML, EAV-Attribute und REST/GraphQL-Endpunkte gegen Bypässe absichern
10. Zusammenfassung
Die wichtigste Erkenntnis der Input-Validation-Strategien lässt sich in einem Satz zusammenfassen: Eine Allowlist definiert, was erlaubt ist, und lehnt implizit alles andere ab, während eine Blacklist stets nur bekannte Angriffsmuster erfasst und zwangsläufig Lücken hat. Serverseitige Validierung ist nicht verhandelbar, clientseitige Prüfung ist reine UX-Convenience. filter_var() deckt viele Standardfälle ab, verlangt aber strikte Vergleiche und ein klares Verständnis, was jeder Filter tatsächlich prüft. Validierungsbibliotheken wie Respect/Validation und Symfony Validator machen komplexe Regelketten lesbar und testbar.
In Magento greifen Form-XML-Regeln nur im Browser, Magento\Framework\Validator und EAV-Attributvalidierung dagegen serverseitig, allerdings nur auf den Pfaden, die tatsächlich durch das Attributmodell laufen. Kanonisierung vor der Prüfung verhindert, dass kodierte Payloads eine Allowlist umgehen. Und die klare Trennung zwischen Validierung, die ablehnt, und Sanitization, die transformiert, verhindert die gefährliche Annahme, eine für die HTML-Ausgabe bereinigte Eingabe sei automatisch für jeden anderen Kontext sicher.
Input-Validation-Strategien: Whitelist statt Blacklist, das Wichtigste auf einen Blick
Allowlist statt Blacklist
Nur explizit erlaubte Werte akzeptieren, alles andere per Default ablehnen. Kein Wettlauf gegen neue Bypass-Techniken.
Serverseitig ist Pflicht
Client-Validierung ist nur UX-Komfort, niemals Sicherheitsgrenze. Jeder Request kann sie umgehen.
filter_var() richtig einsetzen
Strikte Vergleiche, Bounds-Optionen nutzen, Sanitize-Filter niemals als Validierungsersatz.
Validierung vs. Sanitization
Validierung lehnt ab, Sanitization transformiert. Beides sauber trennen und nie miteinander verwechseln.