Egal wie groß oder klein das Webprojekt auch ist, Eingaben von Benutzern stellen fast immer die Grundlage für weitere Aktion dar. Seien es Formulare zur Kontaktaufnahme, Nutzerregistrierung oder Parameterübergaben mittels GET. Gleichzeitig sind Benutzereingaben jedoch auch eine der größten Gefahren für Webanwendungen und daher mit höchster Vorsicht zu behandeln. Denn vom Programmierer werden hier meist nur bestimmte Werte erwartet, in Wahrheit steht es dem Benutzer aber völlig frei, welche Daten er übermittelt. Eine der wichtigsten Grundregeln bzgl. Sicherheit in Webanwendungen lautet deshalb:
Prüfe alle Eingaben!
Für ein aktuelles Projekt war ich deshalb auf der Suche nach einer Möglichkeit eine URL auf Gültigkeit zu prüfen. Denn allzu oft werden in ein Formularfeld zur Angabe der Website einfach Fantasiedomains, wie ich://habe.keine oder http://foo.bar, eingetragen.
Da mit PHP 5.2 die filter_var-Funktion eingeführt wurde, hoffte ich, dass der Parameter FILTER_VALIDATE_URL in Kombination mit FILTER_FLAG_SCHEME_REQUIRED oder FILTER_FLAG_HOST_REQUIRED mich meinem Ziel etwas näher bringen würde. Jedoch mußte ich nach einigen Tests feststellen, dass FILTER_VALIDATE_URL nur sehr bedacht eingesetzt werden sollte. Denn laut filter_var sind beispielsweise auch folgende URLs gültig:
filter_var('keine://domain', FILTER_VALIDATE_URL) !== false; //true filter_var('foo://bar', FILTER_VALIDATE_URL) !== false; //true filter_var('javascript://test%0Aalert(xss)', FILTER_VALIDATE_URL) !== false; //true
Besonders das Beispiel mit dem Javascript-Code zeigt anschaulich, dass filter_var für eine zuverlässige URL-Prüfung in Webanwendungen deshalb ungeeignet ist und zu großen Problemen führen kann.
Daher entschied ich mich, für die Syntaxüberprüfung der URL weiterhin auf reguläre Ausdrücke zu setzen. Als Regex für die URL-Syntax verwende ich eine sehr umfangreiche und gut geteste Variante von Diego Perini. Kombiniert man diesen zusätzlich mit einer Abfrage des HTTP-Statuscodes, lassen sich alle nicht existierenden Domains dadurch ausfiltern.
function urlValidate($url) { $url = trim($url); if(preg_match('%^(?:(?:https?)://)(?:\S+(?::\S*)?@|\d{1,3}(?:\.\d{1,3}){3}|(?:(?:[a-z\d\x{00a1}-\x{ffff}]+-?)*[a-z\d\x{00a1}-\x{ffff}]+)(?:\.(?:[a-z\d\x{00a1}-\x{ffff}]+-?)*[a-z\d\x{00a1}-\x{ffff}]+)*(?:\.[a-z\x{00a1}-\x{ffff}]{2,6}))(?::\d+)?(?:[^\s]*)?$%iu', $url)) { if(ini_get('allow_url_fopen')) { $headers = @get_headers($url, 1); if (preg_match('/^HTTP\/.*\s+(200|401)/', $headers[0])) { return true; } elseif (preg_match('/^HTTP\/.*\s+(300|301|302|303|307|308)/', $headers[0])) { if ($headers !== false && isset($headers['Location'])) { if($headers['Location'] != $url) { return urlValidate($headers['Location']); } else // Never ending story { return false; } } else { return false; } } else { return false; } } elseif (function_exists('curl_version')) { $user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:16.0) Gecko/20121026 Firefox/16.0"; $ch = @curl_init($url); @curl_setopt($ch, CURLOPT_HEADER, 1); @curl_setopt($ch, CURLOPT_NOBODY, 1); @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); @curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); @curl_setopt($ch, CURLOPT_HEADER, 1); @curl_setopt($ch, CURLOPT_TIMEOUT, 5); @curl_setopt($ch, CURLOPT_USERAGENT, $user_agent); @curl_exec($ch); $http_statuscode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if(preg_match('/^200|301|302$/', $http_statuscode)) { return true; } } else { throw new Exception('curl and allow_url_fopen are not avaiable.'); } } return false; }
Der Aufruf der Funktion ist denkbar einfach:
if(urlValidate('http://www.datenreise.de/php-url-korrekte-syntax-und-existenz-pruefen')) { echo "URL existiert"; } else { echo "URL existiert nicht"; }
Um IDN-Domains zu prüfen, müssen diese vor dem Funktionsaufruf in Punycode umgewandelt werden. Beispiel (börse.de):
urlValidate('http://www.xn--brse-5qa.de')
Note: There is a rating embedded within this post, please visit this post to rate it.