Backoffice sécurisée en PHP
[30 mn de lecture - paru le 11/5/2006 6:13:45 PM - Public : Confirmé]
|
   
|
Auteur
2. Sécurisation des opérations (formulaires)
2.1. Avant propos
Toutes les opérations effectuées à partir de formulaires sont soumises à de nombreux risques car les données qu'elles traitent proviennent de l'extérieur et sont donc non sûres.
Il convient donc de vérifier que les types des données reçues sont bien du type attendu et de protéger toutes les données textes des caractères qui pourront être interprétés par le parseur de la base de données et conduire à des injections SQL. Les manières de contrôler et protéger vos données sont abordés dans cet article
Cependant, vérifier les données ne suffit pas ! En effet, toutes vos opérations partent du principe que votre application fait confiance à l'utilisateur et que toutes les actions qu'il initie sont de son propre fait.
C'est à partir de ce principe qu'un hacker va pouvoir abuser de cette confiance et automatiser certaines taches voir faire exécuter des tâches à un utilisateur à son insu via une attaque de Cross-Site Request Forgeries.
2.3. Cryptogramme visuel
Le principe du cryptogramme visuel est de présenter à l'utilisateur une image contenant un code qu'il devra recopier afin de valider l'exécution de l'action demandée.
Cette méthode est surtout utilisée pour protéger les formulaires d'inscription, mais elle peut aussi servir pour les formulaires d'identification ou de changement de mot de passe.
Le principe de fonctionnement est simple, on génère un code, que l'on affiche dans une image et sauvegarde dans la session, lors de la validation du formulaire, on vérifie que le code entré par l'utilisateur est le même que celui stocké dans la session.
Pour cela nous allons utiliser la classe suivante :
class VisualCryptogram
{
/*
* Méthode statique publique generate
* Génère un code aléatoire et renvoit une image
* Aucune valeur de retour
*/
public static function generate() {
// Active la session PHP si ce n'est pas déjà fait
if(session_id() == "")session_start();
/*
* Entêtes de la réponse HTTP
* Spécifie qu'il s'agit d'une image et qu'elle ne doit pas être mise en cache
*/
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Content-type: image/png");
// Génération du code qui sera affiché (Les caractères trop semblables sont supprimés)
$charset = "23456789ABCDEFGHJKMNPQRSTUVWXYZ23456789";
$secucode = "";
for($i = 0 ; $i < 8 ; $i++) $secucode .= $charset[mt_rand(0,strlen($charset)-1)];
// Sauvegarde du code dans la session
$_SESSION['visualcryptogram'] = $secucode;
// Listing des polices utilisées pour l'affichage du code
$fonts = array("fanta___.ttf", "wobbly.ttf","BUNGNIPP.TTF");
// Création de l'image
$img = imagecreate(200, 100);
$white = imagecolorallocate($img , 255, 255, 255);
$black = imagecolorallocate($img , 0, 0, 0);
// Ecriture des lettres
for($i=0 ; $i < strlen($secucode) ; $i++) {
// Choix d'une police au hasard
$font = mt_rand(0, count($fonts)-1);
// Choix d'un angle au hasard
$angle = mt_rand(-13 , 13);
// Ecriture de la lettre
imagettftext($img, 20, $angle, 5 + (34 * $i), 40, $black, $fonts[$font], $secucode[$i]);
}
// Renvoit de l'image
// Le format jpeg permet d'avoir une image de qualité dégradée et donc d'autant plus dur à reconnaître par OCR
imagejpeg($img);
// Suppression de l'image en mémoire
imagedestroy($img);
}
/*
* Méthode statique publique check()
* Vérifie que le code passé en paramètre correspond bien au cryptogramme visuel
*/
public static function check($code) {
// Active la session PHP si ce n'est pas déjà fait
if(session_id() == "")session_start();
if(!isset($_SESSION['visualcryptogram'])) return false;
return $code == $_SESSION['visualcryptogram'];
}
}
Nous pouvons donc afficher l'image avec une page contenant simplement le code suivant :
VisualCryptogram::generate();
Et nous pouvons contrôler si le code tapé est correct de cette manière :
if(VisualCryptogram::check($_POST['cryptogram_code']) { // Code si OK } else { // Code si ERR }
2.4. Système de jetons
Le système précédent permet d'empêcher les attaques de type Cross-Site Request Forgeries puisqu'elle nécessite que l'utilisateur valide lui même l'opération.
Cependant cette méthode est contraignante pour l'utilisateur et ne peut être utilisée pour confirmer toutes les opérations, en particulier celles effectuées dans les applications Web 2.0 qui sont pour la pluspart executées via AJAX sans que l'utilisateur ne s'en rende compte.
Il faut donc trouver un système permettant d'être sûr que les opérations demandées par l'utilisateurs interviennent dans un contexte normal et non dans le cadre d'une attaque.
Pour cela, nous allons mettre en place un système de jeton qui permettra de n'exécuter les opérations que si la demande est accompagnée d'un jeton valide.
Le principe est simple, prenons l'exemple d'une suppression d'élément. Dans la page de confirmation de suppression, nous allons créer un jeton utilisable qu'une seule fois qui devra être transmis lors de la demande de suppression définitive afin de valider son authenticité.
Prenons un deuxième exemple, nous avons une page permettant de déplacer des éléments d'un liste et enregistrons à chaque changement les modifications, il va falloir créer un jeton au chargement de cette page qui aura une durée de vie limitée; renouvelée à chaque utilisation qui devra être transmis lors de toutes les sauvegardes de modifications.
Nous allons utiliser les classes suivantes pour gérer nos jetons :
/*
* Class Tokens
* Fournit des méthodes statiques pour gérer les jetons de la sessions
*/
class Tokens
{
/*
* Méthode statique publique addToken()
* Sauvegarde un jeton dans la session en l'indexant par son code et une clef facultative
*/
public static function addToken($token, $key = "") {
if(!isset($_SESSION['token_list'])) $_SESSION['tokens_list'] = array();
$tokencode = Tokens::generateTokenCode();
$_SESSION['token_list'][$key.'_'.$tokencode] = $token;
return $tokencode;
}
/*
* Méthode statique publique getToken()
* Renvoit un jeton en fonction d'un code et d'une clef ou null si aucun jeton ne correspond
*/
public static function getToken($tokencode, $key = "") {
return isset($_SESSION['token_list'][$key.'_'.$tokencode]) ? $_SESSION['token_list'][$key.'_'.$tokencode] : null;
}
/*
* Méthode statique privée generateTokenCode()
* Renvoit un code unique pour identifier le jeton
*/
private static function generateTokenCode() {
// Ce couplage de fonction permet de générer un jeton quasi unique
// Cependant ce code est plus simple que celui préconisé par la RFC 4122
return uniqid(mt_rand(), true);
}
}
/*
* Interface Token
* Définit les méthodes que doivent implémenter les types de jetons
*/
interface TokenI
{
public function __construct($restriction);
public function isValid();
public function consume();
}
/*
* Classe UsageToken
* Jeton limité en terme d'usage
*/
class UsageToken implements TokenI
{
private $nbmax;
private $nb;
public function __construct($nb_use) {
$this->nb = 0;
$this->nbmax = $nb_use;
}
public function isValid() {
return $this->nb < $this->nbmax;
}
public function consume() {
$this->nb++;
}
}
/*
* Classe UsageToken
* Jeton limité en terme de temps
*/
class TimeToken implements TokenI
{
private $validity;
private $lastconsumption;
public function __construct($validity_time) {
$this->validity = $validity_time;
$this->lastconsumption = time();
}
public function isValid() {
return (time() - $this->lastconsumption > $this->validity);
}
public function consume() {
$this->lastconsumption = time();
}
}
Ainsi, dans votre page qui affiche les formulaire a sécuriser, vous pourrez mettre :
$tokencode = Tokens::addToken(new UsageToken(1), 'votre_clef_interne'); echo '<input type="hidden" name="token" value="'.$token.'" />';
La clef interne vous permet de restreindre le champ d'application des jetons afin que tous vos jetons n'interfèrent pas entre eux et ne puissent servir que pour l'opération pour laquelle ils ont été créés. Ainsi, vous pouvez spécifier la clef 'del_elmt_4' pour votre jeton et vous saurez que ce jeton n'est valide que pour la supression de l'élément d'id 4.
Enfin, vous pourrez tester la validité d'un jeton de la manière suivante :
$token = Tokens::getToken($_POST['tokencode'], 'del_elmt_'.$_POST['id']); if(!is_null($token) && $token->isValid()) { $token->consume(); // Code si OK } else { // Code si ERR }
|