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 ultraconcis 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ée à 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ème, 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 :
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 :
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 :
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 :
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 :
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 :
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.
<?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 :
allow_url_fopen = off
IV-L. magic_quotes_gpc▲
Cette directive permet d'échapper (par backslashes) les quotes, doubles quotes, backslashes et caractères 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().
A 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, doubles quotes, backslashes et caractères 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 :
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 :
<?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 :
AddType application/x-httpd-php .phtml .pwml .php3 .php4 .php .php2 .inc
Pas plus difficile que ça.
Autre solution : ajouter une seconde extension… par exemple « .php »
<?php
$filename .= ".php";
?>
Ou bien encore, toujours dans httpd.conf, interdire l'accès à de tels fichiers :
<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 :
<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 :
$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 :
admin'#
Notre requête deviendrait alors :
$query = "SELECT * FROM users WHERE login = 'admin'#'
AND password = 'jun40h'";
Ce qui à l'exécution donnerait le produit de la requête SQL suivante :
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 par l'internaute.
Il est également recommandé de hasher (via md5()) logins et mots de passe avant de les stocker.
Exemple :
<?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
<?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