Seit PHP5.2 sind die Filter-Funktionen fester Bestandteil von PHP geworden. Jedoch greifen immer noch sehr wenige Programmierer auf diese Filter-Funktionen zurück, was ein kleines bis großes Sicherheitsrisiko darstellt.
Der Normalfall sieht immer noch so aus, dass einfach abgefragt wird ob in $_GET
, $_POST
oder $_REQUEST
ein Schlüssel vorhanden ist und dann der entsprechende Wert verwendet wird. Leider viel zu oft sieht man auch, dass erst gar nicht geprüft wird ob der Schlüssel vorhanden ist und einfach davon ausgegangen wird das dieser vorhanden ist und verwendet werden kann. Das ist nicht nur schlechter Stil, sondern eröffnet auch eine Sicherheitslücke. Denn die superglobalen Arrays $_GET
, $_POST
und $_REQUEST
sind keine Einbahnstraße, man kann aus ihnen lesen und in sie hinein schreiben. Schauen wir uns das mal etwas näher an.
$_REQUEST
lassen wir in der in diesem Artikel mal außen vor da es noch nicht von den Filter-Funktionen unterstützt wird. Zudem sollte man auf $_REQUEST
auch nur in Ausnahmefällen zugreifen. $_REQUEST
Enthält die Daten von $_GET
und $_POST
. Die Verwendung von $_REQUEST
deutet also darauf hin, dass man nicht so wirklich weiß was man tut und einfach auf gut Glück auf ein superglobales Array zugreift in der Hoffnung die gesuchten Daten seien schon irgendwie da.
Es gibt einige wenige Fälle wo der Zugriff auf $_REQUEST
sinnvoll sein kann, man sollte sich jedoch im klaren darüber sein das der Zugriff auf $_REQUEST
eben nicht so sicher ist als wenn man $_GET
und $_POST
mit den Filter-Funktionen abfragt.
$_GET und $_POST sind keine Einbahnstraße
Das man auf die superglobalen Arrays $_GET
und $_POST
nicht nur lesend, sondern auch schreibend zugreifen kann, ist anscheinend nicht allen klar. Schauen wir uns dazu mal dieses Script an
$_POST['nonce'] = 'badnonce'; echo 'Was the nonce send with a POST request?<br>'; echo 'With <code>isset()</code>: ' . ( isset( $_POST['nonce'] ) ? 'yes' : 'no' ) . '<br>'; echo 'With <code>filter_has_var()</code>' . ( filter_has_var( INPUT_POST, 'nonce' ) ? 'yes' : 'no' ) . '<br>'; echo 'Value of <code>nonce</code> with <code>$_POST[\'nonce\']</code>: ' . $_POST['nonce'] . '<br>'; echo 'Value of <code>nonce</code> with <code>filter_input( INPUT_POST, \'nonce\' )</code>:' . filter_input( INPUT_POST, 'nonce' ) . '<br>'; echo '<hr>'; $_GET['nonce'] = 'badnonce'; echo 'Was the nonce send with a GET request?<br>'; echo 'With <code>isset()</code>: ' . ( isset( $_GET['nonce'] ) ? 'yes' : 'no' ) . '<br>'; echo 'With <code>filter_has_var()</code>' . ( filter_has_var( INPUT_GET, 'nonce' ) ? 'yes' : 'no' ) . '<br>'; echo 'Value of <code>nonce</code> with <code>$_GET[\'nonce\']</code>: ' . $_GET['nonce'] . '<br>'; echo 'Value of <code>nonce</code> with <code>filter_input( INPUT_GET, \'nonce\' )</code>:' . filter_input( INPUT_GET, 'nonce' ) . '<br>';
Die nonces
sind in WordPress ein Sicherheitsmechanismus mit dem man prüfen kann ob eine Anfrage tatsächlich von dort kommt, von wo man sie erwartet. Verlassen wir uns darauf das der Schlüssel nonce
vorhanden ist und verwenden diesen, gehen wir das Risiko ein das er an einer anderen Stelle einfach überschrieben wurde.
Das man die Werte der superglobalen Arrays einfach überschreiben kann, lässt sich recht einfach mit vielen WordPress Funktionen testen die Werte aus den superglobalen Arrays $_GET
, $_POST
und/oder $_REQUEST
verwenden.
add_action( 'admin_init', function(){ if ( ( isset( $_GET['action'] ) && 'delete-comment' == $_GET['action'] ) || ( isset( $_GET['action'] ) && 'trash' == $_GET['action'] ) ) { $_GET['action'] = $_POST['action'] = $_REQUEST['action'] = 'not-now'; } } );
Einfach mal diesen Code-Schnipsel in die functions.php
einfügen und dann im Backend die Kommentarübersicht aufrufen. Dort dann wahllos ein paar Kommentare löschen. Entweder über die Checkboxen und eines der Bulk-Menüs oder über den Link der erscheint wenn man mit der Maus auf einen Kommentar zeigt.
Nutzt man die Checkboxen und das Bulk-Menü, passiert gar nichts. Beim Löschen über den Link verschwindet zwar der Kommentar und der Zähler hinter “Papierkorb” geht hoch, ein Klick auf “Alle Kommentare” genügt um die vermeintlich gelöschten Kommentare wieder anzuzeigen.
Es geht aber deutlich böser
add_action( 'admin_init', function(){ if ( ( isset( $_GET['action'] ) && 'delete-comment' == $_GET['action'] ) || ( isset( $_GET['action'] ) && 'trash' == $_GET['action'] ) ) { $a = array_fill( 0, 5, 0 ); array_walk( $a, function( &$e ){ $e = rand( 1,999 ); } ); $_GET['delete_comments'] = $_POST['delete_comments'] = $_REQUEST['delete_comments'] = $a; $_GET['c'] = $_POST['c'] = $_REQUEST['c'] = rand( 1, 999 ); } } );
Dieser Code überschreibt nicht einfach die action
, sondern ersetzt die ausgewählten Kommentare durch zufällige Werte. Egal was man auswählt, welche Kommentaren gelöscht werden ist reiner Zufall. Im besten Fall passiert nichts, da die zufällig ausgewählten IDs nicht existieren. Im schlimmsten Fall werden ganz andere Kommentare gelöscht als ausgewählt. Man könnte auch über eine SQL-Abfrage alle vorhandenen Kommentar-IDS abfragen und dann zufällig IDs auswählen, dass ist dann richtig böse aber ein anderes Thema.
Mit filter_input()
wäre dies nicht möglich, da filter_input()
nur die Werte berücksichtigt, die auch tatsächlich über einen Request abgeschickt wurden (siehe erstes Beispiel). Nun fragt man sich vielleicht warum WordPress etwas so dummes macht wenn es doch eine solch einfache Lösung für das Problem gibt.
Nun die Antwort liegt in meinen ersten Satz dieses Artkels. Die Filter-Funktionen sind erst ab PHP5.2 verfügbar, WordPress wurde jedoch zu PHP4 Zeiten entwickelt und enthält auch noch unendlich viel PHP4-Code. Wer das ändern möchte, kann gerne mal eine Suche nach $_GET
, $_POST
und $_REQUEST
über die WordPress Dateien laufen lassen und die entsprechenden Stellen anpassen. Die Suche nach $_GET
liefert aktuell 594 Treffer in 104 Dateien. Soviel zu diesen Thema.
Ich hoffe es ist klar geworden warum es nicht ganz unerheblich ist seinen Code mindestens auf PHP5.2 Niveau zu bringen und warum man in seinen Themes und Plugins unbedingt auf die Filter-Funktionen setzen sollte anstatt direkt auf die superglobalen Arrays $_GET
, $_POST
und $_REQUEST
zuzugreifen.
Zum Schluss noch etwas Mehrwert. Manchmal kann es vorkommen das Werte sowohl per GET als auch POST Request übergeben werden (meist im Zusammenhang mit Ajax Requests), in diesen Fall greift man gerne auf $_REQUEST
zurück. Da die Filter-Funktionen von PHP $_REQUEST
(noch) nicht unterstützen, müsste man jedes mal umständlich $_GET
und $_POST
abfragen ob der benötigte Schlüssel vorhanden ist. Das kann man sich mit einer kleinen Funktion vereinfachen und den Mangel von PHP gleich etwas ausbügeln.
function read_superglobals( $vars ) { if ( is_string( $vars ) ) $vars = (array) $vars; $return = array(); foreach ( $vars as $variable_name ) { if ( ! filter_has_var( INPUT_POST, $variable_name ) ) { if ( ! filter_has_var( INPUT_GET, $variable_name ) ) { $return[$variable_name] = ''; } else { $return[$variable_name] = filter_input( INPUT_GET, $variable_name ); } } else { $return[$variable_name] = filter_input( INPUT_GET, $variable_name ); } } return $return; }
Die Funktion erwartet als Parameter ein Array oder einen String mit den jeweiligen Schlüssel(n) den/die man abfragen möchte. Die Funktion prüft dann mit filter_input()
ob in $_GET
oder $_POST
der Schlüssel vorhanden ist. Ist er vorhanden, kopiert sie den Wert mit den zugehörigen Schlüssel in ein Array und gibt anschließend das Array zurück. Die Funktion arbeitet in etwa so wie wp_reset_var()
, jedoch mit den Unterschied das sie keine globalen Variablen erzeugt und ausschließlich die Schlüssel zurück gibt, die auch tatsächlich mittels eines GET oder POST Requests abgesendet wurden.
Natürlich kann man die Funktion auch dann verwenden, wenn man genau weiß ob die Werte mittels GET oder POST Request gesendet wurden. Dann bekommt man ein Array zurück mit exakt den Schlüssel-Werte Paaren die man benötigt.