Actual-PC: Sécurité PHP: Les grands principes
 
Logo Actual-PC
Votre site d'aide informatique
flux RSS Actual-PC

Sécurité PHP: Les grands principes



logo PHPNous allons voir dans ce tutoriel les principes de sécurité à appliquer en PHP, en quoi consistent les attaques de pirates et comment les contrer. C'est une présentation globale destinée à comprendre les concepts d'attaques, ayez conscience qu'il est impossible de décrire toutes les attaques et failles existantes en un seul article.
Les explications et démonstrations ci-dessous ont un but purement pédagogique, je pars du principe que connaitre les attaques reste le meilleur moyen de les contrer. Je ne saurais être tenu responsable en cas d'exploitation de ces informations à des fins illégales. Je vous rappelle également que la loi interdit de procéder à des attaques informatiques sur des systèmes qui ne vous appartiennent pas.

Le langage PHP permet de créer des sites web dynamiques. L'internaute échange des informations avec le site qui s'adapte en fonction des paramètres reçus. C'est justement à ce niveau qu'il faut intervenir pour sécuriser son système.
Rappelez vous toujours que vous ne maitrisez JAMAIS ce qui est envoyé par l'internaute.

I) Les Magic Quotes

Les "magic quotes" sont une fonctionnalité de PHP permettant de protéger automatiquement les données transmises par l'utilisateur ou provenant d'une source externe. Les caractères réservés ' (apostrophe), " (guillemet), \ (antislash) et NULL seront échappés, c'est à dire précédés par un antislash. De ce fait ils seront interprétés comme des caractères.

Si la directive magic_quotes_gpc est activée (On) dans le fichier php.ini, les données transmises par l'utilisateur via HTTP ($_GET, $_POST et $_COOKIE) seront automatiquement échappées.

Cette directive est généralement activée par défaut pour prévenir les injections SQL, mais il est vivement conseillé de la désactiver et de gérer vous même le filtrage des données.
Si elle est désactivée et que vous ne filtrez pas vous-même les valeurs reçues, votre code est vulnérable aux injections SQL. Nous considérerons que la directive est à toujours Off dans la suite du tutoriel.


II) Les vérifications côté utilisateur

L'erreur fréquente est de croire que les vérifications ou contraintes appliquées côté utilisateur garantissent la validité des données.

Le Javascript est souvent employé pour contrôler les informations saisies par l'utilisateur avant l'envoi du formulaire. Il permet entre autre de vérifier qu'un champ a bien été rempli, qu'il est de type numérique, de filtrer certains caractères, ou encore de créer des structures conditionnelles (si case cochée, envoi autorisé), etc. Mais dans la pratique vos efforts sont vains.
Les vérifications de ce type peuvent être contournées très facilement en bloquant l'exécution du Javascript au sein du navigateur! On pourra citer l'extension NoScript de Firefox (Page de l'extension).

Beaucoup pensent également que l'utilisation de listes déroulantes ( balise HTML <select> ), checkboxes ou radio button, pour forcer le choix de valeurs prédéfinies, les protège de toute attaque. Ils s'affranchissent alors de contrôler et filtrer les données reçues. Ceci est très dangereux.
Des extensions comme Firebug pour Firefox permettent d'éditer le code source d'une page très simplement et ainsi de soumettre des données non prévues.

Pour finir de vous convaincre, rien n'empêche le hackeur de créer son propre formulaire (en utilisant bien entendu les mêmes noms de champs), et de le faire pointer vers votre page de traitement (avec action). Il est alors libre d'envoyer tout ce qu'il souhaite pour tenter d'exploiter d'hypothétiques failles.


Prenons l'exemple d'un formulaire permettant d'afficher au choix, la date d'inscription ou le nombre de messages de chaque membre. Voici à quoi pourrait ressembler le formulaire HTML présent sur votre page:
 
  <form action="traitement_formulaire_membres.php" method="post">
    <select name="pseudo">
      <option value="jackSparrow">jackSparrow</option>
      <option value="user445">user445</option>
    </select>
    <input type="radio" name="informations" value="date_inscription" checked />
    <input type="radio" name="informations" value="nb_messages" />
    <input type="submit" value="Afficher infos" />
  </form>
 
La liste des utilisateurs est générée en PHP et provient de la table USERS. Cette table est composée des champs username, password, date_inscription, nb_messages et admin.
Les administrateurs ne sont pas affichés (mais il existe un utilisateur Administrateur dont le mot de passe est Gty56jM).


Code du fichier traitement_formulaire_membres.php:
 
  // Réception des paramètres
  $pseudo = $_POST['pseudo'];
  $info_a_afficher= $_POST['informations'];
 
  // Requête SQL pour récupérer les informations nécessaires
  $requete = "SELECT ".$info_a_afficher." FROM USERS WHERE username='".$pseudo."'";
  $query_result = mysql_query($requete);
  $query_fetch = mysql_fetch_array($query_result); // On stocke le résultat dans un tableau structuré
 
  echo "Utilisateur: ".$pseudo."<br />";
 
  if(isset($query_fetch['date_inscription'])){ // Si existant dans le tableau alors afficher
    echo "Date inscription: ".$query_fetch['date_inscription']."<br />";
  }
 
  if(isset($query_fetch['nb_messages'])){
    echo "Nb messages: ".$query_fetch['nb_messages']."<br />";
  }
 
Ce code est vulnérable et permet de récupérer les mots de passe de tous les utilisateurs et administrateurs. Comment est-ce possible?
Regardons le formulaire créé par l'attaquant:
 
  <form action="http://www.site_cible.com/traitement_formulaire_membres.php" method="post">
    <select name="pseudo">
      <option value="Administrateur">Administrateur</option>
    </select>
    <input type="radio" name="informations" value="password as date_inscription" checked />
    <input type="submit" value="Envoyer" />
  </form>
 
Lorsque le hackeur va cliquer sur le bouton Envoyer, votre page traitement_formulaire_membres.php va être appelée.
Avec les paramètres envoyés, la requête devient:
 
  SELECT password AS date_inscription FROM USERS WHERE username='Administrateur';
 
Et voici ce qui sera affiché à l'attaquant:
 
  Utilisateur: Administrateur
  Date inscription: Gty56jM
 
En lieu et place de la date d'inscription se trouve le mot de passe de l'administrateur!
Pour éviter ce type d'attaque, définissez directement les champs à récupérer dans le code PHP et filtrez toutes les données récupérées.

Il est indispensable de contrôler en PHP à la réception du formulaire, TOUS les champs qui le compose (même ceux que l'utilisateur n'a pas à saisir) et sont utilisés dans une requête SQL.

III) Les attaques par injection SQL

Quasiment tous les sites web dynamiques ont recours à une base de données SQL pour la gestion du contenu. C'est un vecteur d'attaque connu et très exploité. Mieux vaut donc y être préparé.
Nous utiliserons le système de gestion de base de données MySQL pour les démonstrations qui vont suivre.

Une attaque par injection SQL consiste à détourner la requête initiale, afin de lui faire exécuter une action non prévue par le système.
Cela se fait en exploitant des caractères réservés.

Via les champs de type texte

Prenons l'exemple type d'un formulaire d'identification. Vous disposez de 2 champs Login et Password. La combinaison de ces deux informations permet de donner accès au compte utilisateur. Nous allons considérer l'existence d'un compte dont le login est Aurelien et le mot de passe mDp. Le fait de stocker le mot de passe en clair dans la base de données est également une faille de sécurité, mais cela nous arrange pour la démonstration!
 
  <form action="identification.php" method="post">
    <input type="text" name="login" />
    <input type="text" name="password" />
    <input type="submit" value="Identification" />
  </form>
 
Voici le code du fichier identification.php qui sert à générer notre requête SQL:
 
  mysql_query("SELECT * FROM USERS WHERE login='".$_POST['login']."' AND password='".$_POST['password']."'");
 
Nous décidons de nous identifier avec le compte de l'utilisateur Aurelien. Remplaçons maintenant par les paramètres reçus depuis le formulaire. Nous aboutissons à la requête suivante:
 
  SELECT * FROM USERS WHERE login='Aurelien' AND password='mDp';
 
Une entrée remplissant les conditions existe bien dans la table, la requête va donc retourner les informations de l'utilisateur ( * signifiant tous les champs). Dans certaines conditions, ce code est même susceptible de retourner une erreur SQL pour un utilisateur inoffensif. C'est le cas si l'utilisateur saisit un login ou mot de passe comportant un ou plusieurs apostrophes.

Mettons nous maintenant à la place du pirate.
Imaginons que je saisisse comme login: Aurelien' OR '1=1 et comme password: abcde.
Le pirate va saisir le mot de passe car beaucoup de scripts s'assurent que les 2 champs sont bien remplis.

Voici maintenant la requête avec les paramètres énoncés:
 
  SELECT * FROM USERS WHERE login='Aurelien' OR '1=1' AND password='abcde';
 
La requête VA retourner les informations de l'utilisateur. Comment est-ce possible?

L'utilisateur Aurelien existe bien. Nous avons choisi le login de sorte à altérer la structure globale de la requête: OR '1=1' sera toujours vrai. La condition AND sur le password se trouvant APRÈS le OR (qui provient du login), cette dernière n'a plus d'obligation d'être respectée.
Simplement en connaissant le login de l'utilisateur nous avons pu nous identifier sans même connaitre son mot de passe. En saisissant ' OR '1=1 dans les 2 champs, nous aurions obtenu le même résultat, cette fois-ci sans connaitre le login.

Le concept est ici de se servir des apostrophes qui sont interprétés comme délimiteurs de chaine car non échappés.
Le premier apostrophe sert à terminer la chaine du login, pour ajouter ensuite l'instruction OR suivie d'une expression où les deux opérandes (1) sont encadrées par des apostrophes. A une exception près, l'apostrophe fermante de la deuxième opérande devant être omise car ce sera l'apostrophe prévue pour fermer la chaine du login dans le code PHP qui prendra sa place.
L'exploitation aurait également été possible avec des guillemets si dans le code PHP les valeurs des champs login et password étaient encadrées de guillemets (dans l'exemple il s'agissait d'apostrophes).


Seconde exploitation de la faille: l'utilisation des commentaires SQL.
En SQL, tout ce qui se trouve après # ou -- n'est plus interprété, et est considéré comme commentaire. Imaginons maintenant que je saisisse comme login: Aurelien' -- et comme password: abcde.

Nous obtiendrions la requête suivante:
 
  SELECT * FROM USERS WHERE login='Aurelien'-- AND password='abcde';
 
La condition du password n'est plus prise en compte car considérée comme commentaire par MySQL.

Pour contrer cette attaque je vais vous présenter une solution efficace pour le SGBD (Système de Gestion de Base de Données) MySQL:
  • mysql_real_escape_string(): Fonction PHP qui échappe, c'est à dire ajoute un backslash devant les caractères spéciaux suivants : NULL, \x00, \n, \r, \, ', " et \x1a.

Vous devez donc l'appliquer sur toutes les données reçues (formulaires, paramètres passés via l'URL, $_COOKIE, $_FILES, ...), qui vont être utilisées dans la requête SQL. Le fait d'ajouter un backslash devant un simple quote (apostrophe) permet d'indiquer qu'il doit être interprété comme un caractère et non pas comme un "marqueur" de fin de chaine (un délimiteur).
Cette fonction protège donc vos requêtes SQL pour les champs de type texte.

Le code sécurisé pour notre script d'identification serait:
 
  mysql_query("SELECT * FROM USERS WHERE login='".mysql_real_escape_string($_POST['login'])."' 
               AND password='".mysql_real_escape_string($_POST['password'])."'");
 
En employant les mêmes identifiants que les précédents utilisés pour l'attaque nous obtenons:
 
  SELECT * FROM USERS WHERE login='Aurelien\' OR \'1=1' AND password='abcde';
 
La structure de la requête n'est plus modifiée et le requête ne retournera aucun résultat.

Via les champs de type numérique

Les choses se corsent un peu. Vous le savez peut-être, dans une requête SQL vous ne devez normalement pas encadrer les valeurs numériques par des simples quotes ou guillemets (malgré le fait que le langage le tolère).

Par conséquent il n'est même plus nécessaire d'essayer de modifier la structure en jouant sur les simples quotes et guillemets comme nous l'avons vu plus haut, pour tenter une injection SQL.

L'utilisation de UNION dans une requête SQL permet de combiner ensemble les résultats de deux requêtes. Une restriction de UNION est cependant que toutes les colonnes correspondantes doivent inclure le même type de données.
Pour votre information cette attaque est également possible en utilisant un champ texte non protégé (voir premier exemple).

Mettons nous dans le cas d'un site qui restreint l'affichage des articles en fonction d'un champ publication dans la table ARTICLES. Si le champ publication est à 1 l'article est publié, s'il est à 0 l'article ne sera pas visible. Nous savons qu'un article avec l'id 23 a existé mais n'est maintenant plus disponible.

Nous avons la structure de requête suivante:
 
  mysql_query("SELECT * FROM ARTICLES WHERE publication=1 AND id=$_POST['page_id']");
 
Si on essaye d'afficher une page dont le champ publication est à 0, la requête ne retournera rien car les 2 conditions (publication=1 et id=xxx) ne sont pas satisfaites en même temps.

Le numéro de la page est récupéré en POST, par conséquent l'information provient de l'utilisateur, et comme vous le savez celui-ci est susceptible de la modifier.

Pour afficher l'article dont l'id vaut 4, nous avons la requête:
 
  SELECT * FROM ARTICLES WHERE publication=1 AND id=4;
 
Si je crée maintenant un formulaire spécifique que je fais pointer vers la page de traitement du site visé:
 
  <form action="http://www.site_cible.com/affichage_articles.php" method="post">
    <input type="text" name="page_id" value="9999 UNION SELECT * FROM ARTICLES WHERE id=23" />
    <input type="submit" value="Envoyer" />
  </form>
 
Portez votre attention sur le paramètre value du champ page_id.

Après l'envoi de ce formulaire, nous obtenons la requête suivante:
 
  SELECT * FROM ARTICLES WHERE publication=1 AND id=9999 UNION SELECT * FROM ARTICLES WHERE id=23;
 
J'ai volontairement choisi 9999 comme id pour que la première requête ne retourne rien. Mais comme vous pouvez le voir, la seconde requête injectée ne tient plus compte de la restriction sur le champ publication. Par conséquent la requête va bien retourner l'article ayant l'id 23 alors que celui-ci ne devrait pas être visible. La fonction mysql_real_escape_string() est inefficace contre ce type d'attaque.
Pour éviter ce type d'attaque la solution la plus radicale est de caster (forcer le type) des variables numériques reçues en paramètres, qui vont se retrouver dans une requête SQL.
 
  mysql_query("SELECT * FROM ARTICLES WHERE publication=1 AND id=".(int)$_POST['page_id']);
 
Une chaine vide ou commençant par un caractère deviendra 0. Une chaine composée exclusivement de chiffres sera correctement transformée en entier.
Vous pouvez également faire appel aux fonctions PHP is_numeric(), settype(), ou ctype_digit() qui vérifie si tous les caractères de la chaîne sont des chiffres.
N'oubliez pas que tout ce qui provient de champs de formulaire, même un nombre, est toujours reçu comme une chaine de caractères.

Via les instructions SQL LIMIT ou OFFSET

L'injection énoncée ci-dessous peut être réalisée dans différents cas de figure, mais le positionnement des instructions LIMIT et OFFSET (en fin de requête) et la nature des données qu'elles prennent en paramètres (des entiers) facilitent grandement son exploitation.

Pour limiter le nombre de résultats retournés par une requête, on utilise LIMIT en SQL:
 
  SELECT * FROM NEWS ORDER BY date DESC LIMIT 0, 5;
 
On ne veut récupérer que les 5 dernières news publiées.
Généralement les bornes de LIMIT sont récupérées en paramètres lorsque l'utilisateur clique sur un lien "page suivante".
 
  $query= "SELECT * FROM NEWS ORDER BY date DESC LIMIT ".$_GET['page_start'].", ".$_GET['page_stop'];
 
Si le hackeur a découvert la structure de vos tables, il ne va pas se priver pour semer la pagaille, ou tout simplement s'octroyer quelques droits supplémentaires.
Par l'intermédiaire d'une requête initialement prévue pour afficher des news, le hackeur va pouvoir changer le mot de passe du compte administrateur. Il va pour cela lui suffire de concaténer une seconde requête en utilisant la variable $_GET['page_stop'].

Comme vous le savez, les variables $_GET sont passées en paramètres dans l'URL. Voici donc l'URL à appeler pour afficher les news 0 à 5: http://www.site_cible.com/affichage_news.php?page_start=0&page_stop=5 .

Maintenant que se passe-t-il si on appelle:
http://www.site_cible.com/affichage_news.php?page_start=0&page_stop=; UPDATE USERS SET password='aaaa' WHERE login='Administrateur' .

Les 2 requêtes seront exécutées car elles sont séparées par un point virgule. Le mot de passe de l'administrateur sera alors modifié ce qui permettra au hackeur de s'identifier à la place de l'administrateur avec le mot de passe aaaa. Le risque est donc bien réel.
Mais le hackeur peut choisir tout type de requête: INSERT, DELETE, DROP,... pouvant avoir des conséquences plus ou moins désastreuses.

La solution pour se prémunir de ce type d'attaque est de filtrer tous les points-virgules qui se trouvent dans une requête. Par exemple:
 
  str_replace(";", '', $requete);
 
Malgré cela vous n'échapperez pas à une erreur SQL, mais la requête ne sera pas exécutée, et ne compromettra donc pas la sécurité de votre site.

Stocker le résultat d'une requête

Si le hackeur parvient à détourner une de vos requêtes, il n'est pas dit qu'il parvienne à faire afficher ce qu'il souhaite sur vos pages. Il peut alors choisir de stocker le résultat d'une requête dans un fichier texte (sur votre serveur), qu'il n'aura plus qu'à afficher via son URL. Le fonctionnement dépend des droits d'écriture dans le répertoire de destination.
La redirection de la sortie standard vers un fichier se fait avec l'instruction SQL INTO OUTFILE. Par défaut, le répertoire de destination est /bin/mysql/nom_table. Il faut cependant envoyer le résultat vers le répertoire www/ de l'hébergement du site web, car c'est dans celui-ci que se trouvent les fichiers accessibles au public (pour la navigation).

L'exemple suivant stocke le résultat de la requête dans le fichier resultat_requete.txt du répertoire www/ :
 
  SELECT * FROM USERS INTO OUTFILE '../../www/resultat_requete.txt';
 
Les ../ permettent de remonter d'un niveau dans l'arborescence des fichiers.
Si il n'y a pas de restrictions de droits, le fichier est créé, et contient toutes les données de la table (formaté avec des tabulations). Il est accessible avec un navigateur à l'adresse http://www.site_cible/resultat_requete.txt.


Finalement ce tutoriel nous aura permis de passer en revue les attaques fréquemment utilisées, et de vous montrer comment les parer. Vous l'aurez compris, vous ne devez jamais considérer les données reçues comme sûres, et ne négliger aucune vérification.
A vous maintenant de vous assurer que le code que vous produisez n'est pas sujet aux différentes attaques expliquées tout au long de ce tutoriel!


add comment Ajouter un commentaire

(SANS les 2 premiers caractères !)