Empfehlung des Tages: Filter-Funktionen

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.