Eine der bemerkenswerten Bibliotheken, die PHP zur Nutzung anbietet – sofern sie mal nicht wie so oft überfordert ist – ist die PCRE-Bibliothek (Perl Compatible Regular Expressions), mit der sich auf einfache Weise (naja, wenn man die Syntax einmal verstanden hat) umfangreichste Dinge vollführen lassen. Eine häufige Anwendung, die man hierbei regelmäßig als Betrachter fremder Bibliotheken zu Gesicht bekommt, ist hierbei Quelltext, der elegant Parsing von Eingabedaten vornimmt, Sicherheitsprüfungen ausführt oder auf andere Art einen Eingabestring verarbeitet. Mit ein wenig Magic drumherum lässt sich so ein ziemlich schneller Syntax-Highlighter schreiben, der mit einem Schlag ganze Gruppen von Schlüsselworten highlighted und dabei abhängig von den gefundenen Schlüsselworten z.B. Links auf deren Dokumentation fertigt.
Doch wie so häufig bei PHP gibt es eine Reihe von Fallstricken. Einer dieser liegt bei eben dieser genannten Bibliothek. Diese bietet für umfangreiche, nicht statisch ausführbare Ersetzungen mit preg_replace einen speziellen Modus: Den /e-Modifier, der auf Grund seiner Arbeitsweise auch gern Evil-Modifier genannt werden darf. Aber dazu sollten wir erst einmal klären, was diese Funktion tut.
Die Funktion preg_replace nimmt ein Suchmuster, sowie ein Ersetzungstemplate, gefolgt von dem zu durchsuchenden String. Für jedes Vorkommen des Suchmusters wird nun der gefundene Text (und seine Sub-Matches) entsprechend dem Template eingesetzt. An der Fundstelle wird nun der durch das Templating entstandene String eingesetzt. Dies ist an sich ungefährlich, da zu keiner Zeit Benutzereingaben als Quelltext interpretiert werden.
Naja, nicht ganz: Es gibt wie gesagt oben genannten Evil Modifier. Dieser sorgt dafür, dass das Ersetzungs-Template mit (weitestgehend) unescapeten Werten des gefundenen Matches gefüllt wird. Der hierbei entstehende String wird anschließend mit eval ausgeführt. Gelingt es also nun, die entstehende Ersetzung so zu beeinflussen, dass man seinen Match in den auszuführenden String nicht allein als Teil eines Strings, sondern als laufenden Code einbringen kann, so kann man PHP an dieser Stelle beliebigen Quelltext unterjubeln.
Dies ist insbesondere deshalb problematisch, da in älteren PHP-Versionen das Escaping der in den replacement-Parameter eingesetzten Strings nicht sauber funktionierte. Zudem hat PHP einen regulären Ausdruck auch dann als eval behandelt, wenn man durch einen ungünstigen Zufall irgendwo eine Subexpression wie (?e:blablubb) einschmuggeln konnte; selbst wenn auf oberster Ebene kein eval vorgesehen war.
Warum erzähl ich das? Weil, trotz dessen dass dieses Problem inzwischen seit Jahren bekannt ist, es immer noch genug Software – gerade auch stark verbreitete Projekte – gibt, die preg_replace mit dieser gefährlichen Zutat nutzen, auch wenn die meisten Vorkommen als sicher anzusehen sind. Hierzu gehören z.B. phpBB (sowohl 2 als auch 3), SquirrelMail und bis vor kurzem WordPress (fixed in 2.8).
Da der Evil-Modifier eine Code Injection zulässt, gab es verschiedene Ansätze, von denen der Hardening-Patch und die Suhosin-Extension für PHP zu den bekanntesten zählen dürften. Diese ermöglichem es einem Server-Betreiber diesen Modifier explizit zu deaktivieren und somit effektiv für die Deaktivierung jeglicher eval-Befehle (direkt oder indirekt ausgeführt) zu sorgen. Und genau hier liegt das Problem: Möchte man als Server-Betreiber eine „No-Eval, since it’s Evil“-Policy erzwingen, laufen viele Anwendungen nur mit entsprechenden Modifikationen. Diese upstream einzupflegen wäre Aufgabe der zuständigen Entwickler; doch trifft man hier gerne auf Unverständnis, warum man etwas so Praktisches, wie den Evil-Modifier, entfernt wissen möchte. Und das obwohl es gleichwertige, wenn nicht sogar bessere, Alternativen gibt: preg_replace_callback wäre eine davon. Aber soviel möchten dann viele doch nicht wahrhaben.
Die Nutzung von preg_replace_callback mag auf den ersten Blick durchaus etwas rückschrittlich wirken, ist es aber bei genauerer Betrachtung überhaupt nicht. Nehmen wir einfach ein kleines Beispiel her. Möchte man einen UTF-8-Dekoder schreiben, der jegliche UTF-8-Zeichen in HTML-Entities konvertiert, so bietet sich preg_replace auf Grund seiner Vielseitigkeit an – ab PHP6 gibt es hier zwar auch noch andere Möglichkeiten, aber die lassen wir vorerst beiseite.
Nun findet man also in einem gar nicht so unbekannten Webmail-Client eine eben solche Funktion, die auf den ersten Blick auch durchaus übersichtlich erscheint. Aber gut: Das wollen wir uns einmal genauer anschauen:
/**
* Decode utf-8 strings
* @param string $string Encoded string
* @return string Decoded string
*/
function charset_decode_utf_8 ($string) {
// Some generic checks ...
// ...
// decode six byte unicode characters
$string = preg_replace("/([\374-\375])([\200-\277])([\200-\277])([\200-\277])([\200-\277])([\200-\277])/e",
"'&#'.((ord('\\1')-252)*1073741824+(ord('\\2')-200)*16777216+(ord('\\3')-200)*262144+(ord('\\4')-128)*4096+(ord('\\5')-128)*64+(ord('\\6')-128)).';'",
$string);
// decode five byte unicode characters
$string = preg_replace("/([\370-\373])([\200-\277])([\200-\277])([\200-\277])([\200-\277])/e",
"'&#'.((ord('\\1')-248)*16777216+(ord('\\2')-200)*262144+(ord('\\3')-128)*4096+(ord('\\4')-128)*64+(ord('\\5')-128)).';'",
$string);
// decode four byte unicode characters
$string = preg_replace("/([\360-\367])([\200-\277])([\200-\277])([\200-\277])/e",
"'&#'.((ord('\\1')-240)*262144+(ord('\\2')-128)*4096+(ord('\\3')-128)*64+(ord('\\4')-128)).';'",
$string);
// decode three byte unicode characters
$string = preg_replace("/([\340-\357])([\200-\277])([\200-\277])/e",
"'&#'.((ord('\\1')-224)*4096+(ord('\\2')-128)*64+(ord('\\3')-128)).';'",
$string);
// decode two byte unicode characters
$string = preg_replace("/([\300-\337])([\200-\277])/e",
"'&#'.((ord('\\1')-192)*64+(ord('\\2')-128)).';'",
$string);
// remove broken unicode
$string = preg_replace("/[\200-\237]|\240|[\241-\377]/",'?',$string);
return $string;
}
Was ist hieran nun so böse? Im Grunde 2 Dinge:
- Es wird ein eval ausgeführt, bei dem binäre Daten in den Ausführungskontext des Skriptes eingebunden werden. Dies ist schon per Definition schlecht; nicht zuletzt, weil dies im Falle von PHP sogar ohne ausreichendes Escaping geschieht (neuere Versionen escapen zwar einige Zeichen, das reicht aber u.U. nicht aus).
- Der auszuführende Code entsteht erst zur Laufzeit. Somit kann keine Vorab-Optimierung oder Syntax-Prüfung vorgenommen werden, bevor nicht der endgültig auszuführende Source durch eine Ersetzung aufgebaut wurde. Dieser Schritt kann nicht gecacht werden.
Wöllte man es besser machen, heißt die Antwort somit: Nutzung eines Callbacks. Naiv kann dies wie folgt realisiert werden:
function charset_decode_utf_8_6byte($match) {
// decode six byte unicode characters
return '&#'.((ord($match[1])-252)*1073741824+(ord($match[2])-200)*16777216+(ord($match[3])-200)*262144+(ord($match[4])-128)*4096+(ord($match[5])-128)*64+(ord($match[6])-128)).';';
}
function charset_decode_utf_8_5byte($match) {
// decode five byte unicode characters
return '&#'.((ord($match[1])-248)*16777216+(ord($match[2])-200)*262144+(ord($match[3])-128)*4096+(ord($match[4])-128)*64+(ord($match[5])-128)).';';
}
function charset_decode_utf_8_4byte($match) {
// decode four byte unicode characters
return '&#'.((ord($match[1])-240)*262144+(ord($match[2])-128)*4096+(ord($match[3])-128)*64+(ord($match[4])-128)).';';
}
function charset_decode_utf_8_3byte($match) {
// decode three byte unicode characters
return '&#'.((ord($match[1])-224)*4096+(ord($match[2])-128)*64+(ord($match[3])-128)).';';
}
function charset_decode_utf_8_2byte($match) {
// decode two byte unicode characters
return '&#'.((ord($match[1])-192)*64+(ord($match[2])-128)).';';
}
/**
* Decode utf-8 strings
* @param string $string Encoded string
* @return string Decoded string
*/
function charset_decode_utf_8 ($string) {
//Some generic checks ...
//...
// decode six byte unicode characters
$string = preg_replace_callback("/([\374-\375])([\200-\277])([\200-\277])([\200-\277])([\200-\277])([\200-\277])/",
'charset_decode_utf_8_6byte', $string);
// decode five byte unicode characters
$string = preg_replace_callback("/([\370-\373])([\200-\277])([\200-\277])([\200-\277])([\200-\277])/",
'charset_decode_utf_8_5byte', $string);
// decode four byte unicode characters
$string = preg_replace_callback("/([\360-\367])([\200-\277])([\200-\277])([\200-\277])/",
'charset_decode_utf_8_4byte', $string);
// decode three byte unicode characters
$string = preg_replace_callback("/([\340-\357])([\200-\277])([\200-\277])/",
'charset_decode_utf_8_3byte', $string);
// decode two byte unicode characters
$string = preg_replace_callback("/([\300-\337])([\200-\277])/",
'charset_decode_utf_8_2byte', $string);
// remove broken unicode
$string = preg_replace("/[\200-\237]|\240|[\241-\377]/",'?',$string);
return $string;
}
Wer jetzt genau hinschaut, findet sogar noch mehr:
function charset_decode_utf_8_callback($match) {
$m = $match[0];
switch(strlen($m)) {
case 6:
// decode six byte unicode characters
return '&#'.((ord($m[0])-252)*1073741824+(ord($m[1])-200)*16777216+(ord($m[2])-200)*262144+(ord($m[3])-128)*4096+(ord($m[4])-128)*64+(ord($m[5])-128)).';';
case 5:
// decode five byte unicode characters
return '&#'.((ord($m[0])-248)*16777216+(ord($m[1])-200)*262144+(ord($m[2])-128)*4096+(ord($m[3])-128)*64+(ord($m[4])-128)).';';
case 4:
// decode four byte unicode characters
return '&#'.((ord($m[0])-240)*262144+(ord($m[1])-128)*4096+(ord($m[2])-128)*64+(ord($m[3])-128)).';';
case 3:
// decode three byte unicode characters
return '&#'.((ord($m[0])-224)*4096+(ord($m[1])-128)*64+(ord($m[2])-128)).';';
case 2:
// decode two byte unicode characters
return '&#'.((ord($m[0])-192)*64+(ord($m[1])-128)).';';
default:
return $m;
}
}
/**
* Decode utf-8 strings
* @param string $string Encoded string
* @return string Decoded string
*/
function charset_decode_utf_8 ($string) {
//Some generic checks ...
//...
// decode 2-6 byte unicode characters
$string = preg_replace_callback("/[\374-\375][\200-\277][\200-\277][\200-\277][\200-\277][\200-\277]|".
"[\370-\373][\200-\277][\200-\277][\200-\277][\200-\277]|".
"[\360-\367][\200-\277][\200-\277][\200-\277]|".
"[\340-\357][\200-\277][\200-\277]|".
"[\300-\337][\200-\277]/",
'charset_decode_utf_8_callback', $string);
// remove broken unicode
$string = preg_replace("/[\200-\237]|\240|[\241-\377]/",'?',$string);
return $string;
}
Und siehe da: Wir haben aus 5 Ersetzungen mal eben eine gemacht und dabei dem Parser sogar noch die Chance gegeben, unseren Quelltext vorab zu optimieren. Besser noch: Es gibt keine Stelle mehr, an der Nutzerdaten Teil des auszuführenden Quelltextes sind!
Wenn das mal nichts ist! Und das Gute daran: Selbst in unbekanntem Source lässt sich die für diese Verbesserung notwendige Änderung binnen weniger Minuten mit wenigen Handgriffen durchführen und verbessert dabei sogar nebenbei sogar den Schutz der eigenen PHP-Anwendung gegen Code Injections.
Die Anpassung einer bestehenden Software an die Richtlinie zur Vermeidung von preg_replace mit /e mag für gewachsene Software vielleicht schmerzhaft sein, aber neben GeSHi, Joomla und WordPress sollten wesentlich mehr im großen Stil eingesetzte Anwendungen dafür sorgen, auch auf stark eingeschränkten Systemen zurecht zu kommen. Die Entwickler von oben erwähntem Webmail-Client wollen hier zumindest nachbessern; auch wenn noch kein konkreter Zeitpunkt genannt wurde …
[…] lernen es nie. eval is evil. Dabei wird Quelltext mit einem regulären Ausdruck sofort ausgeführt (evaluiert). Probleme, die […]
Pingback by Der Blog von Clemens Bartz | Tikiwiki und Typolight sind evil — 14.01.2010 @ 02:24:06
[…] die vom Nutzer eingegeben wurden, auswerten möchte, so kann man dies entweder mit Hilfe bösartiger Konstrukte tun, oder aber, mit Hilfe eines Parsers, die Ausdrücke selber auswerten und dann in einer […]
Pingback by Parser-Bau für Einsteiger « BenBE's humble thoughts — 01.03.2011 @ 17:56:14