dimanche, 14 janvier 2007

Form flooding

Les formulaires sont un des points vulnérables d'une application web, car c'est notamment à travers eux que les "clients" peuvent injecter des données dans l'application.

De plus, même si le formulaire est suffisamment protégé contre l'injection de données, qu'elle soit XSS, SQL, string format ou autre, le formulaire reste vulnérable à un flood :

Comment réagit votre formulaire si un client bien authentifié décide de poster 1'000'000 de fois le formulaire ?


Ce genre d'attaque peut avoir des effets très néfastes...

Il y a plusieurs méthodes pour s'en prémunir, et je vais en présenter une qui, contrairement au captcha, ne requiert pas d'intervention de la part de l'utilisateur (on ne peut pas faire de captcha dans un webservice...), mais protège tout de même notre formulaire.

On va donc attribuer à chaque formulaire un identifiant unique, qui est entré dans une table (ou en var de session) conjointement avec un timestamp. L'identifiant nous permettra d'éviter (au pire de remarquer) la soumission multiple du même formulaire, et grâce au timestamp, nous pourrons mesurer le temps entre le chargement de la page contenant le formulaire et son renvoi au serveur, temps qui ne devrait pas être inférieur à une voir plusieurs secondes pour un utilisateur humain, selon la taille du formulaire.

En recevant le formulaire, le serveur peut donc contrôler si :

1. Le formulaire est valide, i.e. l'identifiant du formulaire est valide
2. Le formulaire n'a pas été envoyé plusieurs fois.
3. Le délai de soumission du formulaire n'est pas trop élevé pour une session donnée.

L'implémentation de cette solution est elle aussi multiple, mais j'en donne un exemple ci-dessous :


__toString(); ?> ...
*
* if (isset($_POST)) {
* $canary = Form_Protector::factory($_POST);
* if (!$canary->is_valid()) {
* if ($canary->exists()) {
* // le canary n'existait pas dans la db, afficher un message d'erreur et recharger la page
* } else {
* // le formulaire a été posté trop rapidement, on peut donc compter le nombre de soumissions durant les 5 dernières minutes, et prendre une des actions suivante
* // --> soit on blacklist l'ip un moment,
* // --> soit on sleep(30) pour temporiser (tarpitting)
* }
* } else {
* // le canary est valide, tout est bien dans le meilleur des mondes.
* }
* }
*/

define("_TIME_TO_SUBMIT_FORM", 2); // temps que l'utilisateur fait pour submiter un formulaire
class Form_Protector {

protected $_canary;
protected $_ip;
protected $_date_request;
protected $_date_request;
protected $_exists = false;

public static $input_name = 'form_protector_canary';

protected function __construct($canary = 0, $ip = "") {
$this->_ip = $_SERVER['REMOTE_ADDR'];
if ($canary === 0) {
$this->_canary = rand();
$this->_insert();
} else {
$this->_canary = $canary;
if ($ip !== "") $this->_ip = $ip;
$this->_load();
}
}

protected function _load() {
$query = sprintf("SELECT * FROM form_protector
WHERE canary = %d AND ip = '%s'" . int_val($this->_canary),
mysql_real_secape($this->_ip));
// query,
// load $this->_date_request, $this->_date_request = time();
// si pas un champ est retourné, on passe $this->_exists à true; et on UPDATE ... SET date_response = $this->_date_request
}

protected function _insert() {
$this->_date_request = time();
$query = sprintf("INSERT INTO form_protector (canary, ip, date_request) VALUES (%d, %s, %d)", int_val($this->_canary), mysql_real_secape($this->_ip), $this->_date_request);
// query
// si pas d'erreur : $this->_exists = true;
}

public function is_valid() {
if ($this->_date_request + _TIME_TO_SUBMIT_FORM <>_exists;
}
public function __toString() {
return '<input value="' . $this->_canary . '" name="form_protector_canary" type="hidden">';
}

public static factory($params = NULL) {
if (is_array($params) && isset($params[self::$name])) {
$canary = $params[self::$name];
} else {
if ($params === NULL) {
$canary = 0;
} else {
$canary = $params;
}
}
return new Form_protector($canary);
}
}

?>

Aucun commentaire: