I. pré-requis

Une bonne connaissance (au mieux une bonne pratique) du PHP, des bases de données et du Web sont nécessaires à la bonne compréhension de cet article. Au besoin, on peut consulter mon ultra-concis PHP.

Cela dit, le propos est ici d'être le plus clair, le plus généraliste, et, nous l'espérons, le plus didactique possible.

Le lecteur est supposé également avoir lu la partie consacrée à la sécurité du manuel en ligne, que le présent article se propose de compléter.

Enfin, il n'est peut-être pas inutile de rappeler que la sécurisation des scripts PHP ne saurait se substituer à la sécurisation du serveur sur lequel ils tournent !

D'excellents logiciels libres ont été conçus dans ce but (cf Sécuriser un réseau hétérogène avec des outils libres), n'hésitez pas à les mettre en œuvre…

II. CGI ou SAPI ?

La compilation de PHP en tant que CGI (Common Gateway Interface) étant une pratique de plus en plus rare, nous redirigerons le lecteur vers la page du manuel consacré à ce sujet.

Par ailleurs, un manuel des « bonnes pratiques » dans l'écriture des scripts CGI est disponible ici.

Nous nous concentrerons donc ici sur l'utilisation, beaucoup plus répandue, de PHP installé en tant que module du serveur Web, ou SAPI (Server Application Programming Interface).

III. Principes de base

Non, la sécurité n'est pas (seulement) affaire de spécialistes. C'est l'affaire de tous, à tout instant, depuis la conception du projet, jusqu'à l'utilisation finale du programme et / ou service.

Au-delà de la technique pure, en constante évolution, il s'agit d'acquérir les bonnes pratiques. L'ensemble de ces bonnes pratiques relève de la plus élémentaire prévention, sur laquelle on n'insistera jamais assez.

Voici un florilège de principes essentiels à garder en mémoire en toute circonstance (liste non-exhaustive) :

  • veiller à limiter au strict nécessaire les droits et permissions de l'utilisateur : il vaut mieux l'entendre se plaindre de son manque de liberté plutôt que de constater les dégâts causés par une trop grande libéralité (principe universel) ;
  • ne jamais faire confiance aux données transmises par l'utilisateur (encore moins à l'internaute !), quand bien même vous auriez élevé les cochons ensemble…
  • toujours tester l'existence / la validité d'un fichier / code à inclure : et s'il n'était pas celui que vous croyiez ?
  • un contrôle des données côté client est bien pratique, mais illusoire quant à la sécurité : quoi de plus simple que de désactiver Javascript ? Seul un contrôle côté serveur peut prétendre être efficace ;
  • préférer, quand cela est possible, la méthode POST à la méthode GET : les variables dans l'url, ça fait mauvais genre ;) ;
  • ne pas confondre complexité et sécurité : si votre système de sécurité vous échappe, considérez que vous n'êtes pas en sécurité. Bon, de toute façon, vous n'êtes jamais en sécurité ;
  • s'efforcer de tenir à jour système serveur et interpréteur PHP avec les dernières versions stables : quand la catastrophe aura eu lieu, ce sera ça de moins à vous reprocher…

IV. Le fichier php.ini

Que vous installiez PHP vous-même ou que vous passiez par un hébergeur, la sécurisation de vos scripts dépend pour une grande part de la configuration du fichier php.ini.

Voici les différentes options de configuration concernées :

IV-A. safe_mode

Lorsque cette option est activée ( safe_mode = on ) :

  • un certain nombre de fonctions sont limitées ou désactivées (celles dédiées aux commandes systèmes, notamment). On trouvera ici une liste des fonctions affectées par l'activation du safe_mode ;
  • une vérification stricte des permissions d'accès des fichiers (basées sur l'UID et le GID) est faite sur les fonctions concernées ;
  • les variables d'environnement sont protégées.

IV-B. safe_mode_exec_dir

Grâce à cette directive, il est possible de spécifier un ou plusieurs répertoires dans lesquels les fonctions affectées par le safe_mode seront pleinement disponibles.

Exemple :

 
Sélectionnez
safe_mode_exec_dir = /private_dir/. 

IV-C. safe_mode_allowed_env_vars

Seules les variables commençant par les préfixes cités ici peuvent être modifiées (par défaut : safe_mode_allowed_env_vars = PHP_ et c'est très bien comme ça).

IV-D. safe_mode_protected_env_vars

Via cette directive, on peut fournir une liste des variables d'environnement qu'un script ne pourra jamais modifier (par défaut : safe_mode_protected_env_vars = LD_LIBRARY_PATH).

IV-E. disable_fonctions

Comme son nom l'indique, cette directive permet de désactiver les fonctions spécifiées. Les noms de fonctions doivent être séparés par une virgule. Cette directive est indépendante du safe_mode.

Exemple :

 
Sélectionnez
disable_functions = exec, system, passthru, popen, mail

IV-F. open_basedir

Cette directive (indépendante du safe_mode) limite les permissions d'accès aux seuls dossiers spécifiés (la racine étant document_root), récursivement.

Exemple :

 
Sélectionnez
open_basedir = /pub_dir/

IV-G. display_errors

En phase de développement, il est bon d'avoir le maximum d'infos sur ce qui pourrait nuire à la bonne, et sécure exécution des scripts (variables et constantes non-définies, essentiellement).

Donc :

 
Sélectionnez
error_reporting = E_ALL

En phase de production, il est sage de ne pas laisser ce genre de messages d'erreur s'afficher à l'écran :

Warning: readfile() failed (No such file or directory) in /var/www/perso/confidentiel.php on line 356.

Donc :

 
Sélectionnez
display-errors = off

IV-H. log_errors

Une fois activée (log_errors = on), cette directive permettra la redirection des messages d'erreur (dont on a interdit l'affichage à l'écran) vers les fichiers logs (unix), dans les journaux d'évènements (win), ou dans des fichiers spécifiés dans la directive suivante.

IV-I. error_log

Permet de spécifier le fichier dans lequel seront écrits les messages d'erreur.

Exemple :

 
Sélectionnez
error_log = /var/php/logs/

IV-J. register_globals

Permet de rendre globales ou non les variables EGPCS (Environment, Get, Post, Cookie, Session).

Il est vivement conseillé de désactiver cette directive (register_globals = off), ne serait-ce que pour s'obliger à prendre les bonnes habitudes en utilisant les variables superglobales.

Succinctement, les variables héritées des méthodes EGPCS ne seront plus disponibles telles quelles, mais accessibles seulement via les superglobales correspondantes.

 
Sélectionnez
 <?php
  if (!$_SESSION['auth']) die ("Vous n'êtes pas authentifié");
 ?>

IV-K. allow_url_fopen

Permet de traiter les url comme des fichiers, d'où la porte ouverte à des plaisanteries douteuses (cf la faille include()).

En conséquence :

 
Sélectionnez
allow_url_fopen = off

IV-L. magic_quotes_gpc

Cette directive permet d'échapper (par backslashes) les quotes, double quotes, backslashes et caractère NULL des chaînes de caractères issues des méthodes Get, Post et Cookies.

Rappelons que, pour pouvoir être passés sous forme de requête SQL, ces caractères doivent être échappés via la fonction addslashes().

À contrario, pour l'affichage d'une chaîne de caractères à l'écran sans backslashes, on aura recours à la fonction stripslashes().

La raison pour laquelle cette directive doit être activée ( magic_quotes_gpc = on ) est que les attaques par injection SQL (cf les bases de données) utilisent très souvent les quotes non échappées.

Bon article sur les magic_quotes : phpinfo.net/articles/article_magic-quotes.html.

IV-M. magic_quotes_runtime

Comme la précédente, cette directive permet d'échapper les quotes, double quotes, backslashes et caractère NULL, mais des chaînes de caractères provenant d'une ressource externe (base de données, zone de texte, etc).

Pour les mêmes raisons que précédemment : magic_quotes_runtime = on.

IV-N. include_path

Spécifie le chemin des pages à inclure : ne pas oublier de renseigner cette directive (cf la faille include()) !

Exemple :

 
Sélectionnez
include_path = /var/htaccessed_dir/

IV-O. file_uploads

Autorise l'upload de fichiers sur le serveur via un script PHP. À désactiver, bien sûr, si on n'en a pas besoin.

V. La faille include()

Cette faille a fait l'objet d'une littérature abondante, grâce à quoi elle a pratiquement disparu aujourd'hui.

Le but du jeu consiste à ne pas permettre à n'importe qui d'inclure n'importe quelle page, que ce soit pour accéder à des données sensibles, ou pour simplement d'effacer votre belle page d'accueil ;).

Outre la désactivation de allow_url_fopen, qui interdira l'inclusion de fichiers distants, voici une manière très simple de se prémunir :

 
Sélectionnez
 <?php
 
  $filename = "./page.php";
 
  if (file_exists($filename)) include($filename);
 
 ?>

Une excellente mesure consiste à regrouper les fichiers de bibliothèques dans un répertoire protégé par .htaccess.

Il est également recommandé d'éviter de donner l'extension .inc aux fichiers à inclure. En effet, si le serveur n'est pas configuré pour interpréter les fichiers avec cette extension, il est tout bêtement possible de les télécharger ou, encore plus bêtement, que le code source s'affiche à l'écran… Pas terrible :(

Cela dit, si vous avez accès à votre httpd.conf, il suffit de rajouter l'extension « .inc » ici :

 
Sélectionnez
 AddType application/x-httpd-php .phtml .pwml .php3 .php4 .php .php2 .inc

Pas plus difficle que ça.

Autre solution : ajouter une seconde extension… par exemple « .php »

 
Sélectionnez
 <?php
  $filename .= ".php";
 ?>

Ou bien encore, toujours dans httpd.conf, interdire l'accès à de tels fichiers :

 
Sélectionnez
 <Files ~ ".inc">
     Order allow,deny
     Deny from all
     Satisfy All
 </Files>

On le voit, les façons de se protéger sont multiples et variées. L'essentiel étant de comprendre, comme toujours, la nature du danger.

VI. les bases de données

De même que sécuriser ses scripts n'a pas grand sens si le système ne l'est pas, il fortement conseillé de bien connaître son SGBD, particulièrement bien sûr ses mécanismes de sécurité.

Le principal souci d'une BDD exposée sur le web provient des injections SQL évoquées plus haut.

Sans rentrer dans des considérations avancées, rappelons seulement que le principe de ce type d'attaque est de passer des requêtes « imprévues » au SGBD, le plus souvent via les champs d'un formulaire, et grâce à l'emploi astucieux de caractères spéciaux ("'", "%", "*", "#", etc).

Un exemple très connu d'injection SQL repose sur l'idée de « couper », à l'aide de commentaires, une partie de la requête à l'exécution.

Soit un banal formulaire d'identification :

 
Sélectionnez
 <form method="post" action="auth.php">
 login : <input type="text" name="login"><br />
 password : <input type="password" name="password"><br />
 <input type="submit" value="OK">

Et une requête, encore plus banale, utilisant les variables issues du formulaire :

 
Sélectionnez
 $query = "SELECT * FROM users WHERE login = '".$_POST['login'].
                            "' AND password = '".$_POST['password']."'";

Pour peu que le pirate connaisse un login « sensible » (sans en connaître le mot de passe), il pourrait essayer de renseigner ainsi le champ « login » du formulaire :

 
Sélectionnez
 admin'#

Notre requête deviendrait alors :

 
Sélectionnez
 $query = "SELECT * FROM users WHERE login = 'admin'#'
                               AND password = 'jun40h'";

Ce qui a l'exécution donnerait le produit de la requête SQL suivante :

 
Sélectionnez
 SELECT * FROM users WHERE login = 'admin'

PHP ignorant tout ce qui se trouve après le signe '#' ne transmettra pas la dernière partie de la requête… Plus besoin de mots de passe ;).

Pour vous prémunir contre ces désagréments, vous avez à votre disposition tout un arsenal de fonctions qui vous permettent de filtrer (ou « parser ») les données transmises pas l'internaute.

Il est également recommandé de hasher (via md5()) logins et mots de passe avant de les stocker.

Exemple :

 
Sélectionnez
 <?php
  $login = trim(htmlspecialchars(addslashes($_SESSION['login'])));                 // parsing du login
  $password = trim(htmlspecialchars(addslashes($_SESSION['password'])));           // parsing du password
  $query = "INSERT INTO clients VALUES (md5($login), md5($password))";             // insertion des variables hashées
 ?>

À toute fin utile, rappelons qu'il est essentiel de créer un utilisateur de la base dont les droits seront restreints au strict minimum (lecture seule si possible).

Si votre BDD contient des données réellement sensibles, le serveur doit être configuré pour supporter les connexions via SSL (Secure Socket Layer, www.openssl.org).

VII. Les sessions

L'usage des sessions est un moyen sûr, simple, efficace et largement utilisé de « tracer » ses visiteurs (sites de commerce en ligne, forums, etc).

Le principe en est simple : il s'agit d'attribuer un identifiant unique à chaque visiteur (par défaut PHPSESSID), ce qui créera un fichier dans un répertoire temporaire sur le serveur, fichier dans lequel seront stockées les variables générées par la session.

Il suffit alors d'appeler (avant tout affichage html) la fonction session_start() sur chaque page où l'on souhaite utiliser ces variables.

Cela peut se faire de différentes façons :

  • automatiquement (via la directive session.auto_start), méthode déconseillée ;
  • via un formulaire dont les champs renseignés seront filtrés : méthode conseillée ;
  • en passant par le tableau superglobal $_SESSION pour la récupération des variables : toujours ;
  • avec ou sans cookies, sachant que s'ils sont bien pratiques pour conserver les variables d'une session l'autre, les cookies peuvent être refusés par le client :(.

Là encore, un minimum de vigilance est requis, afin de protéger chaque identifiant, et prévenir d'éventuelles usurpations d'identité.

La première mesure élémentaire à prendre consiste à stocker logins et mots de passe dans une base de données (et au besoin relire la section précédente).

Une bonne idée est de récupérer (via la variable superglobale $_SERVER['REMOTE_ADDR']) l'ip du visiteur, afin de garantir son caractère unique

 
Sélectionnez
 <?php
  if (isset($ip) && $ip == $_SERVER['REMOTE_ADDR']) {
   echo "Welcome home, $login !";
   }
  else {
   header('Location: login.php');
   }
?>

VIII. Références

Ten Security Checks for PHP : http://www.onlamp.com/pub/a/php/2003/03/20/php_security.html

Creating a Secure PHP Login Script : http://www.devarticles.com/art/1/485

(Beaucoup) plus sur les injections SQL : L'injection (My)SQL via PHP

Un tuto simple et bien construit sur la sécurité des applis Web : Secure your url