Dienstag, 22. April 2008
CSRF Protection (Updated)
Nachdem fukami beim letzten Bonner WebMontag erläutert hatte was CSRF ist und was man alles damit anrichten kann, war mir zunächst nicht ganz klar, wie so etwas in der Praxis ausgenutzt wird und welche Schutzmaßnahmen man dagegen implementieren kann.
Netterweise demonstrierte mir mein Kollege Lary Strojny anhand meines Projekts todoyo.de wie so etwas genau aussieht. Er schickte mir einen unscheinbaren Link, den ich natürlich sofort öffnete und gelangte auf eine einfache Seite mit 3 Images, welche allerdings nicht richtig geladen wurden. Die Bilderpfade lauteten in etwa "http://todoyo.de/task/delete/id/1234". Wäre ich nun bei todoyo eingeloggt gewesen, wären ohne, dass ich etwas gemerkt hätte, einige meiner Aufgaben gelöscht worden. Die IDs der Aufgaben besorgte er sich aus der öffentlichen todoyo-Entwicklerliste. Glücklicherweise war ich nicht eingeloggt ;)
Dadurch wurde mir schlagartig klar, wie CSRF Angriffe funktionieren und welchen Schaden man damit alles anrichten kann, wenn die Seite nicht entsprechend geschützt ist.
Wie sehen nun die Gegenmaßnahmen aus, um so etwas zu verhindern? Folgende Schritte habe ich unternommen:
Netterweise demonstrierte mir mein Kollege Lary Strojny anhand meines Projekts todoyo.de wie so etwas genau aussieht. Er schickte mir einen unscheinbaren Link, den ich natürlich sofort öffnete und gelangte auf eine einfache Seite mit 3 Images, welche allerdings nicht richtig geladen wurden. Die Bilderpfade lauteten in etwa "http://todoyo.de/task/delete/id/1234". Wäre ich nun bei todoyo eingeloggt gewesen, wären ohne, dass ich etwas gemerkt hätte, einige meiner Aufgaben gelöscht worden. Die IDs der Aufgaben besorgte er sich aus der öffentlichen todoyo-Entwicklerliste. Glücklicherweise war ich nicht eingeloggt ;)
Dadurch wurde mir schlagartig klar, wie CSRF Angriffe funktionieren und welchen Schaden man damit alles anrichten kann, wenn die Seite nicht entsprechend geschützt ist.
Wie sehen nun die Gegenmaßnahmen aus, um so etwas zu verhindern? Folgende Schritte habe ich unternommen:
- Aufgaben-IDs aus öffentlichen Listen entfernt
- Bei allen Formularen POST Requests erzwungen
- und anschließend nur POST Variablen auswerten
- Tokens in jedes Formular eingebaut
- Tokens auch für Links zu Datenänderungen benutzen (z.B. einfaches Löschen von Aufgaben per Link statt per Formular)
Im Code sieht das Ganze jetzt so aus:
<?php
// Todoyo/View/Helper/CsrfKey.php
class Todoyo_View_Helper_CsrfKey
{
protected $_token = '';
public function csrfKey()
{
if (empty($_SESSION['tokens'])) {
$_SESSION['tokens'] = array();
}
if (empty($this->_token)) {
$this->_token = md5(uniqid(rand(), TRUE));
$_SESSION['tokens'][] = $this->_token;
$_SESSION['tokens'] = array_slice($_SESSION['tokens'], -100);
}
return $this->_token;
}
}
// Todoyo/Controller/Action.php
class Todoyo_Controller_Action extends Zend_Controller_Action
{
// Prüft ob aktuelle Action geschützt ist und ob CSRF Key gültig ist
public function checkCsrfKey($actions)
{
// Keine CSRF geschützen Actions? Weiter
if (empty($actions)) {
return true;
}
// Action nicht eine der CSRF geschützen Actions? Weiter
if (!in_array($this->_request->getActionName(), $actions)) {
return true;
}
$token = $this->_getParam('x');
// Kein Token oder Token nicht in Session? Abbruch!
if (empty($token) or !in_array($token, (array) $_SESSION['tokens'])) {
$this->_flash->addMessage('Unerlaubte Aktion');
$this->_redirect('/');
return false;
}
// Token war gültig? Token löschen und weiter
$pos = array_search($token, $_SESSION['tokens']);
unset($_SESSION['tokens'][$pos]);
return true;
}
// Prupft ob für aktuelle Action POST erzwungen wurde und wirklich POST ist
public function checkPost($actions)
{
// Keine POST Actions erzwungen? Weiter
if (empty($actions)) {
return true;
}
// Action keine erzwungene POST Action? Weiter
if (!in_array($this->_request->getActionName(), $actions)) {
return true;
}
// Action ist kein POST? Abbruch!
if (!$this->_request->isPost()) {
$this->_flash->addMessage('Fehler beim Verarbeiten der Formulareingaben');
$this->_redirect('/');
}
return $this->checkCsrfKey($actions);
}
}
// TaskController.php
class TaskController extends Todoyo_Controller_Action
{
public function init()
{
parent::init();
$this->checkCsrfKey(array('insert', 'delete'));
$this->checkPost(array('insert'));
}
}
// index/index.phtml
<form action="/task/insert" method="post">
<input type="text" name="title" value="">
<input type="hidden" name="x" value="<?=$this->csrfKey()?>">
<input type="submit" name="submit" value="Speichern">
</form>
<a href=/task/delete/id/1234/x/<?=$this->csrfKey()?>>Aufgabe löschen</a>
Was geschieht im Detail?
Im Template wird nun bei jeder Aktion, die geschützt werden soll, ein CSRF Key (Token) entweder als Hidden Input Feld eingefügt, oder bei Links mit an die URL angehangen. Der CsrfKey-View-Helper liefert bei jedem Aufruf einen neuen zufälligen String, der in der Session gespeichert wird. Pro Seitenaufruf wird ein einziger Token erzeugt und für alle Elemente verwendet. Die Anzahl der gültigen Tokens wurde auf 100 beschränkt, um die Session nicht unnötig aufzublähen und dennoch gültige Requests bei mehreren Tabs/Seiten zu ermöglichen.
Nach dem Abschicken eines Formulars, wird bevor die eigentliche Action ausgeführt wird, geprüft, ob die Aktion durch einen POST-Request entstanden sein muss, was bei Formularen klar der Fall ist. Der Methode "checkPost" werden also alle Actions übergeben, bei denen wir POST erzwingen wollen. Zusätzlich wir bei definierten Actions geprüft, ob sie einen CSRF Key besitzen müssen, und falls ja, ob dieser auch gültig ist (=in der Session vorhanden ist). Wenn ja, dann wird der benutzte Token aus der Session gelöscht und die Action wir ausgeführt. Bei der Verwendung von "checkPost()" wird automatisch auch der CSRF Key überprüft, weil alle Formulare zwingend einen gültigen Token haben müssen, URLs hingegen nur bei Bedarf.
Somit kann man für jeden Controller ganz einfach definieren, welche Actions ausschließlich per POST aufgerufen werden dürfen und welche durch CSRF geschützt werden sollen. In der Regeln ist dies bei allen Aktionen der Fall, bei denen Daten geändert werden (insert, update, delete,...). Bei Anzeige-Aktionen legt man eher Wert auf schöne URLs ohne einen Token.
Für weitere Verbesserungsvorschläge oder Fehlerhinweise wäre ich sehr dankbar :)
UPDATE:
Im deutschen Zend Framework Forum wurden ein paar Vorschläge geäußert:
- Auslagerung der "checkPost()" und "checkCsrf()" Funktionalität in einen eigenen ActionHelper
- Nutzung von Zend_Session_Namespace zum Zugriff auf die Session Tokens
- Erstellung eines Zend_Form_Element um z.B. automatisch bei jedem Form das Hidden Token Element hinzuzufügen
- Bei allen Formularen POST Requests erzwungen
- und anschließend nur POST Variablen auswerten
- Tokens in jedes Formular eingebaut
- Tokens auch für Links zu Datenänderungen benutzen (z.B. einfaches Löschen von Aufgaben per Link statt per Formular)
Im Code sieht das Ganze jetzt so aus:
<?php
// Todoyo/View/Helper/CsrfKey.php
class Todoyo_View_Helper_CsrfKey
{
protected $_token = '';
public function csrfKey()
{
if (empty($_SESSION['tokens'])) {
$_SESSION['tokens'] = array();
}
if (empty($this->_token)) {
$this->_token = md5(uniqid(rand(), TRUE));
$_SESSION['tokens'][] = $this->_token;
$_SESSION['tokens'] = array_slice($_SESSION['tokens'], -100);
}
return $this->_token;
}
}
// Todoyo/Controller/Action.php
class Todoyo_Controller_Action extends Zend_Controller_Action
{
// Prüft ob aktuelle Action geschützt ist und ob CSRF Key gültig ist
public function checkCsrfKey($actions)
{
// Keine CSRF geschützen Actions? Weiter
if (empty($actions)) {
return true;
}
// Action nicht eine der CSRF geschützen Actions? Weiter
if (!in_array($this->_request->getActionName(), $actions)) {
return true;
}
$token = $this->_getParam('x');
// Kein Token oder Token nicht in Session? Abbruch!
if (empty($token) or !in_array($token, (array) $_SESSION['tokens'])) {
$this->_flash->addMessage('Unerlaubte Aktion');
$this->_redirect('/');
return false;
}
// Token war gültig? Token löschen und weiter
$pos = array_search($token, $_SESSION['tokens']);
unset($_SESSION['tokens'][$pos]);
return true;
}
// Prupft ob für aktuelle Action POST erzwungen wurde und wirklich POST ist
public function checkPost($actions)
{
// Keine POST Actions erzwungen? Weiter
if (empty($actions)) {
return true;
}
// Action keine erzwungene POST Action? Weiter
if (!in_array($this->_request->getActionName(), $actions)) {
return true;
}
// Action ist kein POST? Abbruch!
if (!$this->_request->isPost()) {
$this->_flash->addMessage('Fehler beim Verarbeiten der Formulareingaben');
$this->_redirect('/');
}
return $this->checkCsrfKey($actions);
}
}
// TaskController.php
class TaskController extends Todoyo_Controller_Action
{
public function init()
{
parent::init();
$this->checkCsrfKey(array('insert', 'delete'));
$this->checkPost(array('insert'));
}
}
// index/index.phtml
<form action="/task/insert" method="post">
<input type="text" name="title" value="">
<input type="hidden" name="x" value="<?=$this->csrfKey()?>">
<input type="submit" name="submit" value="Speichern">
</form>
<a href=/task/delete/id/1234/x/<?=$this->csrfKey()?>>Aufgabe löschen</a>
Im Template wird nun bei jeder Aktion, die geschützt werden soll, ein CSRF Key (Token) entweder als Hidden Input Feld eingefügt, oder bei Links mit an die URL angehangen. Der CsrfKey-View-Helper liefert bei jedem Aufruf einen neuen zufälligen String, der in der Session gespeichert wird. Pro Seitenaufruf wird ein einziger Token erzeugt und für alle Elemente verwendet. Die Anzahl der gültigen Tokens wurde auf 100 beschränkt, um die Session nicht unnötig aufzublähen und dennoch gültige Requests bei mehreren Tabs/Seiten zu ermöglichen.
Nach dem Abschicken eines Formulars, wird bevor die eigentliche Action ausgeführt wird, geprüft, ob die Aktion durch einen POST-Request entstanden sein muss, was bei Formularen klar der Fall ist. Der Methode "checkPost" werden also alle Actions übergeben, bei denen wir POST erzwingen wollen. Zusätzlich wir bei definierten Actions geprüft, ob sie einen CSRF Key besitzen müssen, und falls ja, ob dieser auch gültig ist (=in der Session vorhanden ist). Wenn ja, dann wird der benutzte Token aus der Session gelöscht und die Action wir ausgeführt. Bei der Verwendung von "checkPost()" wird automatisch auch der CSRF Key überprüft, weil alle Formulare zwingend einen gültigen Token haben müssen, URLs hingegen nur bei Bedarf.
Somit kann man für jeden Controller ganz einfach definieren, welche Actions ausschließlich per POST aufgerufen werden dürfen und welche durch CSRF geschützt werden sollen. In der Regeln ist dies bei allen Aktionen der Fall, bei denen Daten geändert werden (insert, update, delete,...). Bei Anzeige-Aktionen legt man eher Wert auf schöne URLs ohne einen Token.
Für weitere Verbesserungsvorschläge oder Fehlerhinweise wäre ich sehr dankbar :)
UPDATE:
Im deutschen Zend Framework Forum wurden ein paar Vorschläge geäußert:
- Auslagerung der "checkPost()" und "checkCsrf()" Funktionalität in einen eigenen ActionHelper
- Nutzung von Zend_Session_Namespace zum Zugriff auf die Session Tokens
- Erstellung eines Zend_Form_Element um z.B. automatisch bei jedem Form das Hidden Token Element hinzuzufügen
Geschrieben von Marc Jakubowski
in todoyo.de, ZendFramework
um
13:12
Kommentare (0) | Trackbacks (0)
Kommentare (0) | Trackbacks (0)
Trackbacks
Trackback für spezifische URI dieses Eintrags
Keine Trackbacks
Kommentare
Ansicht der Kommentare:
(Linear | Verschachtelt)
Noch keine Kommentare
Kommentar schreiben
