Host-Header-Validierung in PHP - Request-Authority als Eingabe statt Konfiguration behandeln
Eine kleine PHP-Anwendung muss manchmal ihre eigene öffentliche Adresse kennen. Sie erstellt vielleicht einen Link zum Zurücksetzen eines Passworts, erzeugt eine kanonische URL oder leitet Besucher auf die HTTPS-Version einer Seite um. Eine bequeme Abkürzung besteht darin, ein Schema vor $_SERVER['HTTP_HOST'] zu setzen. Doch wer hat diesen Host gewählt: der Betreiber der Anwendung, der Reverse Proxy oder der Client, der gerade diese Anfrage stellt?
Diese Unterscheidung ist wichtig, weil ein Request-Header unbemerkt zur Anwendungskonfiguration werden kann. Das Feld Host hat eine legitime Aufgabe im Protokoll, aber „vorhanden und syntaktisch gültig“ bedeutet nicht „für diese Anwendung autorisiert“. Eine überschaubare Schutzmaßnahme besteht darin, drei Entscheidungen getrennt zu halten: Welcher virtuelle Host nimmt die Anfrage entgegen, welche Authorities akzeptiert die Anwendung und welchen konfigurierten Origin verwendet sie beim Erzeugen öffentlicher Links?
Was Host identifiziert
Eine HTTP-URL enthält eine Authority: normalerweise einen Hostnamen und bei Bedarf einen Port. In HTTP/1.1 transportiert das Feld Host diese Information für das übliche Request-Target in Origin-Form. HTTP/2 und HTTP/3 drücken dieselbe Rolle normalerweise mit dem Pseudo-Header :authority aus. RFC 9110 Abschnitt 7.2 beschreibt diese Authority-Information als Teil des Routings einer Anfrage und der Identifikation ihrer Zielressource.
Dadurch können mehrere Websites dieselbe Adresse nutzen. Ein Webserver kann Traffic auf einem Socket empfangen und anhand des angeforderten Namens einen virtuellen Host auswählen. Die Dokumentation zur Request-Verarbeitung in Nginx erklärt, dass Nginx die Auswahl zunächst über Listen-Adresse und Port eingrenzt und danach den Namen der Anfrage mit server_name vergleicht. Passt kein Name, verarbeitet der Default-Server des jeweiligen Listen-Sockets die Anfrage.
HTTP/1.1 legt außerdem grundlegende Regeln für Nachrichten fest. RFC 9112 Abschnitt 3.2 verlangt, dass ein Client Host sendet. Ein Server muss mit 400 Bad Request antworten, wenn das Feld fehlt, mehrfach vorkommt oder einen ungültigen Wert enthält. Diese Prüfungen beantworten eine Frage zu Syntax und Framing. Sie beantworten nicht die Deployment-Frage: Sind preview.example.com, example.com:8443 oder eine andere gültige Domain Namen, die diese Anwendung tatsächlich bedienen soll?
Wie ein Routing-Wert zum Sicherheitsproblem wird
Das Risiko beginnt, wenn Code die Request-Authority als vertrauenswürdige Wahrheitsquelle behandelt. PHP stellt Request-Header über Servervariablen bereit. Das PHP-Handbuch zu $_SERVER weist darauf hin, dass die Einträge vom Webserver erzeugt werden und Request-Header Namen mit dem Präfix HTTP_ erhalten. Die genauen Variablen hängen von der Server-Schnittstelle ab, doch HTTP_HOST sollte weiterhin als Request-Eingabe und nicht als Deployment-Geheimnis behandelt werden.
Betrachten wir einen Reset-Endpunkt, der diesen Link erzeugt:
$resetUrl = 'https://' . $_SERVER['HTTP_HOST']
. '/reset?token=' . rawurlencode($token);
Wenn die umgebende Infrastruktur eine unerwartete Authority akzeptiert und die Anwendung diese URL per E-Mail versendet, kann der Empfänger ein gültiges Token in einem Link erhalten, der auf den falschen Origin verweist. Dieses Muster wird häufig als Password-Reset-Poisoning bezeichnet. Es ist nicht in jedem Deployment automatisch ausnutzbar: Routing, Umschreiben durch Proxys, E-Mail-Verhalten, Token-Lebensdauer und weitere Kontrollen spielen ebenfalls eine Rolle. Dennoch lässt die Anwendung unnötigerweise eine einzelne Anfrage bestimmen, wohin ein sensibler Link führt.
Der gleiche Vertrauensfehler kann absolute Weiterleitungen, kanonische Links, Cache-Einträge oder Back-End-Routing beeinflussen. PortSwiggers Leitfaden zum Host-Header dokumentiert diese möglichen Folgen und empfiehlt, request-abgeleitete Hosts für absolute URLs möglichst zu vermeiden. Die wichtige Erkenntnis ist enger als „der Host-Header ist gefährlich“: Authority ist eine nützliche Eingabe für das Routing, aber nicht automatisch eine sichere Konfiguration für jede nachgelagerte Entscheidung.
Drei Entscheidungen trennen
1. Am Edge nur bekannte Namen routen
Konfigurieren Sie einen expliziten virtuellen Default-Server, damit Anfragen nicht an die Anwendung durchfallen. Eine minimale Nginx-Struktur sieht so aus:
server {
listen 80 default_server;
server_name _;
return 400;
}
server {
listen 80;
server_name example.com www.example.com;
# Normal application locations go here.
}
Die tatsächlichen TLS- und IPv6-Listener müssen in einer realen Konfiguration separat behandelt werden. Der Unterstrich ist hier lediglich ein konventioneller Nicht-Domainname; default_server bestimmt den Fallback für den Listen-Socket. Die Antwort 400 ist außerdem eine lokale Richtlinienentscheidung und keine Behauptung, jede nicht passende, aber syntaktisch gültige Authority verstoße gegen HTTP.
2. Die Anwendung ihre eigene akzeptierte Menge durchsetzen lassen
Die Filterung am Edge ist wertvoll, doch die Anwendung könnte auch über einen anderen Listener, einen Entwicklungsserver oder eine geänderte Proxy-Route erreichbar sein. Für ein kleines Deployment mit einer festen Menge von ASCII-Domainnamen ist ein exakter Vergleich leichter zu prüfen als ein großzügiger Parser:
<?php
$allowedAuthorities = [
'example.com',
'www.example.com',
];
$authority = strtolower($_SERVER['HTTP_HOST'] ?? '');
if (!in_array($authority, $allowedAuthorities, true)) {
http_response_code(400);
header('Content-Type: text/plain; charset=utf-8');
exit('Unrecognized request authority.');
}
Dieser Guard normalisiert absichtlich nur wenig. Er akzeptiert unterschiedliche Groß- und Kleinschreibung, weil DNS-Hostnamen nicht zwischen Groß- und Kleinschreibung unterscheiden, und verwirft danach alles, was nicht ausdrücklich aufgeführt ist. Ein unerwarteter Port, ein abschließender Punkt, ein Suffix wie example.com.attacker.test, Whitespace, ein IP-Literal oder ein Entwicklungsalias scheitert, sofern das Deployment diese exakte Form nicht bewusst ergänzt.
Dieses enge Verhalten ist kein universeller Authority-Parser. Internationalisierte Domainnamen, IPv6-Literale, dynamische Tenant-Domains und Anwendungen, die bewusst mehrere Ports bedienen, benötigen eine dokumentierte Richtlinie für Kanonisierung und Onboarding. Ein Framework bietet möglicherweise bereits eine solche Funktion. Die Django-Dokumentation zu ALLOWED_HOSTS definiert beispielsweise eigenes Verhalten für Matching, Ports, Wildcards und abschließende Punkte. Das PHP-Beispiel sollte einen geprüften Host-Validierungsmechanismus des Frameworks nicht ersetzen.
3. Öffentliche URLs aus Konfiguration erzeugen
Die Validierung beantwortet, ob eine Anfrage fortgesetzt werden darf. Die Erzeugung kanonischer URLs beantwortet eine andere Frage. Hat eine Website einen öffentlichen Origin, wird er einmal konfiguriert:
$publicOrigin = rtrim((string) getenv('APP_PUBLIC_ORIGIN'), '/');
if ($publicOrigin !== 'https://example.com') {
throw new RuntimeException('APP_PUBLIC_ORIGIN is not configured safely.');
}
$resetUrl = $publicOrigin
. '/reset?token=' . rawurlencode($token);
Die literale Prüfung dient als Illustration für einen einzelnen festen Produktions-Origin. Eine reale Anwendung kann ihre Konfiguration stattdessen beim Start validieren. Entscheidend ist, dass eine eingehende Anfrage nicht den Origin einer E-Mail, eines Webhook-Callbacks, eines kanonischen Tags oder einer anderen öffentlichen absoluten URL bestimmt. Relative Links sind noch einfacher, wenn der Empfänger keine absolute URL benötigt.
Ein Reverse Proxy ändert die Quelle, nicht den Vertrauensbedarf
Ein Proxy kann public.example empfangen und eine Verbindung zu einem Upstream namens php.internal herstellen. Er kann die ursprüngliche Authority in Host beibehalten oder sie in Forwarded beziehungsweise X-Forwarded-Host ablegen. Keiner dieser Namen wird allein deshalb vertrauenswürdig, weil er „forwarded“ enthält.
Die Anwendung sollte eine weitergeleitete Authority nur dann verwenden, wenn der direkte Peer ein bekannter Proxy ist und dieser Proxy alle vom Client gelieferten Kopien entfernt, bevor er seinen eigenen Wert schreibt. Andernfalls kann ein Client die angeblichen Proxy-Metadaten selbst liefern. Dies ist dasselbe Boundary-Prinzip wie bei weitergeleiteten Client-Adressen, aber Daten und Folgen unterscheiden sich: Hier betrifft die Entscheidung die öffentliche Authority, die eine Anfrage repräsentiert.
Es gibt keine einzelne Header-Regel, die für jede Proxy-Kette passt. Dokumentieren Sie, welche Komponente die öffentliche Authority besitzt, wie jeder Hop sie umschreibt und welche Werte die endgültige Anwendung akzeptiert. Benötigt die Anwendung nur einen kanonischen öffentlichen Origin, vermeidet eine feste Konfiguration diese Frage für erzeugte Links häufig vollständig.
Den Ablehnungspfad testen, nicht nur den Happy Path
Senden Sie auf einem System, das Ihnen gehört oder für dessen Prüfung Sie autorisiert sind, Anfragen mit der erwarteten Authority und bewusst unerwarteten Varianten. Prüfen Sie Edge und Anwendung getrennt. Sinnvolle Fälle sind:
- der Produktionshostname mit gewöhnlicher Variation der Groß- und Kleinschreibung;
- ein nicht zugehöriger Hostname;
- ein Suffix-Trick wie
example.com.attacker.test; - ein nicht konfigurierter Port oder ein abschließender Punkt;
X-Forwarded-Host, das direkt von einem Client geliefert wird;- Weiterleitungen, Reset-E-Mails, kanonische Tags und jede Antwort mit einer absoluten URL.
Eine abgelehnte Authority sollte nicht auf eine URL umgeleitet werden, die aus derselben Authority erzeugt wurde. Das würde nicht vertrauenswürdige Eingabe nur in einer weiteren Antwort spiegeln. Prüfen Sie auch Cache-Verhalten und Logs, ohne sensible Reset-Tokens versehentlich in Diagnoseausgaben zu speichern.
Was diese Abwehr nicht beweist
Eine Allowlist beweist weder, dass DNS auf das erwartete Ziel zeigt, noch dass ein TLS-Zertifikat gültig, ein Proxy korrekt isoliert oder die Cache-Schlüsselbildung sicher ist. Sie repariert keine schwachen Reset-Tokens und keine unsichere Ausgabe-Kodierung. Umgekehrt verhindert korrektes TLS nicht zwangsläufig, dass eine Anwendung nach Erreichen eines erlaubten Endpunkts einen absoluten Link aus einem manipulierten Request-Wert erzeugt.
Multi-Tenant-Plattformen können einen Prozess zur Prüfung des Domainbesitzes ebenfalls nicht durch eine breite Wildcard ersetzen und das Problem damit für gelöst erklären. Sie benötigen eine vertrauenswürdige Zuordnung von Tenants zu verifizierten Domains sowie Regeln für Aktivierung, Entfernung und Kanonisierung. Das Beispiel mit exakter Liste ist gerade deshalb nützlich, weil sein Umfang kleiner ist.
Fazit
Das Feld Host ist kein zufälliges HTTP-Detail. Es hilft einem Server, die Authority zu identifizieren, die ein Client erreichen möchte. Der Fehler besteht darin, diesen Request-Wert für eine weitere Rolle einzusetzen: als dauerhafte Anwendungskonfiguration.
Für eine kleine PHP-Website ist eine vertretbare Anordnung überschaubar: unbekannte Namen am Edge ablehnen, in der Anwendung eine enge akzeptierte Menge durchsetzen, weitergeleitete Authority nur über eine explizite Proxy-Grenze vertrauen und öffentliche absolute URLs aus Konfiguration erzeugen. Keine dieser Schichten ist magisch. Zusammen machen sie die entscheidende Frage sichtbar: Welche Komponente darf dieser Anwendung ihren Namen geben?
