Als uns mit PHP 5.2 die filter_var-Funktion geschenkt wurde, war die Zeit solcher Monster vorbei (hier entliehen):
$urlregex = "^(https?|ftp)\:\/\/([a-z0-9+!*(),;?&=\$_.-]+(\:[a-z0-9+!*(),;?&=\$_.-]+)?@)?[a-z0-9+\$_-]+(\.[a-z0-9+\$_-]+)*(\:[0-9]{2,5})?(\/([a-z0-9+\$_-]\.?)+)*\/?(\?[a-z+&\$_.-][a-z0-9;:@/&%=+\$_.-]*)?(#[a-z_.-][a-z0-9+\$_.-]*)?\$"; if (eregi($urlregex, $url)) {echo "good";} else {echo "bad";}
Die simple, aber effektive Syntax:
filter_var($url, FILTER_VALIDATE_URL)
Als dritten Parameter können Filter-Flags übergeben werden, im Bezug auf die URL-Validierung gibt es die folgenden 4 Kandidaten:
FILTER_FLAG_SCHEME_REQUIRED FILTER_FLAG_HOST_REQUIRED FILTER_FLAG_PATH_REQUIRED FILTER_FLAG_QUERY_REQUIRED
Dabei sind die ersten beiden FILTER_FLAG_SCHEME_REQUIRED und FILTER_FLAG_HOST_REQUIRED default.
Ans Eingemachte
So, dann schauen wir uns doch mal ein paar kritische Kandidaten an:
filter_var('http://example.com/"><script>alert("xss")</script>', FILTER_VALIDATE_URL) !== false; //true
Gut, hat ja auch niemand gesagt, dass der URL-Filter XSS bekämpfen soll – also ok. Weiter im Takt:
filter_var('php://filter/read=convert.base64-encode/resource=/etc/passwd', FILTER_VALIDATE_URL) !== false; //true
Schon kritischer. Ein beliebiges Schema macht den Filter glücklich. http(s) und ftp hätte ich mir ja noch gefallen lassen. Potentiell problematisch. Demnach dann auch ok:
filter_var('foo://bar', FILTER_VALIDATE_URL) !== false; //true
Und die Krönung zum Schluss
filter_var('javascript://test%0Aalert(321)', FILTER_VALIDATE_URL) !== false; //true
Schauen wir grad mal genauer hin: javascript ist das Schema. Klar, in die Browser-Adresszeile javascript:alert(1+2+3+4); eingeben und los gehts:
Ist das Grundprinzip von Bookmarklets und auch kein Geheimnis. Aber weiter: Der doppelte // ist ein gewöhnlicher Javascript-Kommentar, überzeugt aber filter_var davon, dass es sich um ein valides URLSchema handelt – siehe die Beispiele oben. Dann kommt die Zeichenfolge %0A, was genau der Output des folgenden Codes ist:
echo urlencode("\n");
Dämmerts? Durch das URL-encoded newline wird der eingeleitete Javascript-Kommentar beendet und es folgt beliebiger Javascript-Code. Stellen wir uns eine Dating-Seite vor, bei der Nutzer-URLs mit filter_var validiert werden und dann 1:1 dargestellt werden. Böses Einfallstor.
Und nun?
Zumindest eine händische Anpassung folgender Form könnte sich bewähren:
function validate_url($url) { $url = trim($url); return ((strpos($url, "http://") === 0 || strpos($url, "https://") === 0) && filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED | FILTER_FLAG_HOST_REQUIRED) !== false); }
Aber selbst nach dieser Anpassung kommt die doch sehr ungewöhnliche URL http://x durch die Validierung durch. Vielleicht sind die Regex-Monster doch nicht so schlecht ;). Ach, bevor ichs vergesse: filter_var ist nicht Multibyte-URL-fähig. Die absolut korrekte URL http://???????.com wird rejected:
var_dump(filter_var("http://???????.com", FILTER_VALIDATE_URL) !== false); //bool(false)
Also: filter_var mit Bedacht einsetzen und an den jeweiligen Kontext anpassen. Abschließend möchte ich noch auf diese schöne Aufstellung an URLs in Abhängigkeit der verschiedenen filter_var – Flags verweisen.