Security-Fix für REDAXOs TinyMCE-AddOn

Inhalt
Das Symptom
Das Standard-TinyMCE-Modul aus REDAXO enthält einen Fehler, über den es möglich ist, (fast) beliebigen Code auszuführen. Der Inhalt des Editors wird über die HEREDOC-Syntax abgerufen. In dieser findet jedoch die Interpolation von bestimmten Elementen (bekanntestes Beispiel sind einfache Variablen) statt. Über die {${foo()}}-Syntax ist es nun möglich, Code auszuführen.
Folgende Eingaben sollte man im Backend lieber nicht tätigen:
$REX{${print("hallo")}}{${print_r($REX)}}{${mysql_query("UPDATE rex_user SET name = 'Pwned!' WHERE 1")}}hallo
EOD;
noch mehr text.
Die ersten vier Eingaben sind noch relativ harmlos. Die letzte wird hingegen zu einem Parse Error führen (Achtung: Der Artikel kann dann auch nicht mehr ohne Weiteres im Backend bearbeitet werden!) und vermutlich nur bei deaktiviertem JavaScript funktionieren, da der TinyMCE sonst keine Zeilenumbrüche zulässt.
Bei Installationen mit nur einem Benutzer dürfte sich dieses Problem nicht äußern, da man sich eher selten selber hacked. Wenn allerdings in einem REDAXO mehrere Benutzer mit teilweise eingeschränkten Rechten existieren, kann es gut möglich sein, dass Alice und Bob nach ihrer Kündigung erstmal noch die halbe Datenbank löschen, bevor sie ihren Schreibtisch aufräumen. Immerhin können sie problemlos Datenbank-Queries ausführen lassen.
Der Patch
Unser Patch baut auf das TinyMCE-AddOn aus REDAXO 4.2.1 auf und verändert es so, dass zwei neue Variablen eingeführt: REX_SQ_VALUE für Single-Quoted-Strings und REX_DQ_VALUE für Double-Quoted-Strings. Diese neuen Platzhalter können, wenn das AddOn aktiv ist, in allen Modulen genutzt werden. Sie werden wie folgt genutzt:
$foo = 'REX_SQ_VALUE[1]';
$bar = "Mi nomo estas REX_DQ_VALUE[1].";Durch die gesonderte Angabe des enthaltenen Strings ist es möglich, ein korrektes Escaping durchzuführen.
Download
Der Patch ist unter MIT-Lizenz veröffentlicht und kann hier in Form einer Patchdatei (für die Nerds unter unseren Lesern) oder als gezip’tes AddOn heruntergeladen werden.
- TinyMCE-AddOn (vollständig)
- TinyMCE-AddOn (nur geänderte Dateien)
- Patch-Datei (direkt in tinymce/ anzuwenden)
Das grundlegende Problem, Texte über HEREDOC-Syntax auszulesen, ist damit natürlich nicht behoben. Wer kann, sollte zur NOWDOC-Syntax aus PHP 5.3 greifen. Alle anderen sollten aufpassen, nicht aus Versehen Eingaben von Besuchern über HEREDOCs auszulesen.
Etwas genauer: Was ist das Problem?
Das Problem entsteht, weil REDAXO versucht, die Slices der Artikel zu cachen. Das ist auch absolut richtig und wünschenswert. Nur ist dabei die Anforderung, dass die Slice-Inhalte (die wir über REX_VALUE[1] usw. abholen) direkt in die Cache-Datei eingebettet werden.
Wenn wir im Modul also
$heading = 'REX_VALUE[1]'; // Überschrift abrufenschreiben, landet bei der Eingabe "Hallo Welt" dann der folgende Code in der Cache-Datei:
?><?php if ($this->ctype == '1' || ($this->ctype == '-1')) { ?><?php
$heading = 'Hallo Welt'; // Überschrift abrufen
?><?php } ?>Auf diese Weise kann REDAXO beim Aufruf des Artikels einfach die Cache-Datei einbinden und muss die einzelnen Werte nicht extra noch aus der Datenbank holen.
Unangenehm wird die Sache, wenn beliebige Strings (Zeichenketten) im Backend eingegeben werden sollen: Der einzige Wert, sie sicher zu holen, ist, sie HTML-kodiert abzurufen. Das ist aber nicht immer erwünscht.
Es gibt ein paar Möglichkeiten, dieses Problem anzugehen.
Variante 1: HEREDOC
Diese Variante haben wir schon auseinandergenommen und als fehlerhaft beschrieben. Im Modul stünde dazu Code wie
$content = <<<MYTEXT
REX_HTML_VALUE[1]
MYTEXT;In HEREDOCs werden Variablen interpoliert (= ersetzt). In der Liste ganz oben in diesem Beitrag finden sich Beispiele, was dann alles möglich ist. HEREDOCs sind also keine gute Wahl.
Immerhin verhindert das HTML-Escaping von REDAXO, dass Methoden von Objekten aufgerufen werden, da alle -> durch -> ersetzt werden.
$content = <<<MYTEXT
Hallo Welt!
Variablen werden ersetzt: $REX.
Methoden können aber nicht aufgerufen werden: $foo->bar().
Außerdem könnte man mittels "MYTEXT;" in einer einzelnen Zeile
aus dem HEREDOC ausbrechen.
MYTEXT;Von allen Problemen können wir nur die Variableninterpolation entschärfen: Variablen werden entfernt, indem das HEREDOC in einer extra dafür angelegten Funktion stattfindet, in der auch noch alle Superglobalen ($_GET, $_POST, ...) via unset() entfernt wurden.
Variante 2: Output Buffer
Statt den Inhalt direkt in einer Variable zu importieren, starten wir einen Output Buffer, packen den Inhalt außerhalb des PHP-Bereichs und fangen ihn danach mit ob_get_clean() wieder ein.
Leider kostet das doch wieder etwas Performance. Außerdem entstehen, wenn Sonderzeichen nicht richtig escaped weden, enorme Sicherheitsprobleme. Wenn PHP-Code nicht korrekt verarbeitet wird, kann ein Modulinhalt selber ausbrechen und machen, was er will. Sogar mehr als in einem HEREDOC möglich wäre. Dummerweise wird durch das Escaping auch Information zerstört (= der String verändert). Es ist danach nicht möglich, die Original-Eingabe zu 100% wiederherzustellen.
<?php
// Modul
ob_start();
?>
REX_VALUE[1]
<?php
$content = ob_get_clean();
// Erzeugte Cache-Datei (Beispiel)
ob_start();
?>
Hallo Welt, ich bin die Moduleingabe!
Man stelle sich nur vor, was passiert wenn
<?php print "beliebiger"; ?> Code erlaubt ist.
Immerhin kann ich ja auch <? ob_end_clean() ?>
den Output Buffer selber schließen, oder sogar
<? exec('format C:\\'); ?> die Festplatte
formatieren...
<?php
$content = ob_get_clean();
?>Kein schöner Gedanke, oder?
Variante 3: Strings
Beim Einbetten von Eingaben in Strings muss beachtet werden, dass es keine Eingabe geben darf, die aus dem String ausbricht. Zu diesem Zweck müssen die jeweils umgebenden Anführungszeichen entsprechend escaped werden. Das kennen wir ja alle.
<?php
$value = 'Foo\'bar ist "toll"!'; // Escape: ' und \
$value = "Foo\"bar ist 'cool'!"; // Escape: ", \ und $
?>Das können die REDAXO-Variablen leider nicht leisten, da sie keine Information darüber haben, ob und von welchen Stringdelimitern sie umgeben sind.
Von hier aus bis zum Patch war es dann nicht mehr weit.
Das Dollarzeichen muss übrigens escaped werden, damit "foo $REX" nicht möglich ist. Zu diesem Zweck kann man einfach addcslashes (nicht mit addslashes() verwechseln!) benutzen und die bösen Zeichen beim Aufruf mit angeben.
