Warum URL-Validierung mit filter_var keine gute Idee ist

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:

Javascript-URL

Javascript-URL

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.