1
0
mirror of https://github.com/php/doc-fr.git synced 2026-03-23 22:52:18 +01:00
Files
archived-doc-fr/security/database.xml
2026-03-02 13:40:31 +01:00

490 lines
20 KiB
XML

<?xml version="1.0" encoding="utf-8"?>
<!-- $Revision$ -->
<!-- EN-Revision: 7204e2dbb9b484c8b67bb5ad4a93fa1369c5b317 Maintainer: lacatoire Status: ready -->
<!-- Reviewed: no -->
<chapter xml:id="security.database" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Sécurité des bases de données</title>
<simpara>
De nos jours, les bases de données sont des composants incontournables
des serveurs web et des applications en ligne, qui fournissent du contenu
dynamique. Des données secrètes ou critiques peuvent être stockées dans les
bases de données : il est donc important de les protéger efficacement.
</simpara>
<simpara>
Pour lire ou stocker des informations, il faut se connecter au serveur
de bases de données, envoyer une requête valide, lire le résultat et
refermer la connexion. De nos jours, le langage le plus courant pour ce
type de communication est le langage SQL
(<literal>Structured Query Language</literal>). Voir
comment un pirate peut
<link linkend="security.database.sql-injection">s'introduire dans une
requête SQL</link>.
</simpara>
<simpara>
Comme on peut s'en douter, <acronym>PHP</acronym> ne peut pas protéger
les bases de données par lui-même. La section suivante présente les
notions de base pour protéger les bases de données, lors de la programmation
de scripts <acronym>PHP</acronym>.
</simpara>
<simpara>
Gardez bien cette règle simple en tête : la défense se fait par couches.
Plus il y a de tests pour protéger la base, plus faible sera la
probabilité de réussite d'un pirate. En ajoutant à cela un bon schéma de base
de données, on obtient une application réussie.
</simpara>
<sect1 xml:id="security.database.design">
<title>Schéma de base de données</title>
<simpara>
La première étape est de créer une base de données, à moins d'utiliser
une base de données déjà créée. Lorsque la base
de données est créée, elle est assignée à un propriétaire, qui a
exécuté l'instruction de création.
Généralement, seul le propriétaire et le super utilisateur peuvent
intervenir avec les tables de cette base, et il faut que ce dernier
donne des droits à tous les intervenants qui auront à travailler sur cette
base.
</simpara>
<simpara>
Les applications ne doivent jamais se connecter au serveur de
bases de données sous le nom du propriétaire ou de l'administrateur,
car ces utilisateurs ont des droits très importants, et pourront
exécuter n'importe quelle requête, comme la
modification de tables, l'effacement de lignes ou même encore,
la destruction de la base.
</simpara>
<simpara>
Il est possible de créer différents utilisateurs de bases de données pour
chaque aspect de l'application, avec des droits limités aux
seules actions planifiées. Il faut alors éviter que le même utilisateur
dispose des droits de plusieurs cas d'utilisation. Cela permet que
si des intrus obtiennent l'accès à la base avec l'un de ces jeux
de droits, ils ne puissent pas affecter toute l'application.
</simpara>
</sect1>
<sect1 xml:id="security.database.connection">
<title>Connexions au serveur de base de données</title>
<simpara>
Il est recommandé d'établir des connexions au serveur avec le
protocole SSL, pour chiffrer les échanges clients/serveur, afin
d'améliorer la sécurité. Il est aussi possible d'utiliser un client
SSH pour chiffrer la connexion entre les clients et le serveur
de bases de données. Si l'une de ces deux protections est
mise en place, il sera difficile de surveiller le trafic et
de comprendre les informations échangées.
</simpara>
</sect1>
<sect1 xml:id="security.database.storage">
<title>Modèle de stockage avec chiffrement</title>
<simpara>
Les protocoles SSL/SSH protègent les données qui circulent entre
le serveur et le client : SSL/SSH ne protège pas les données
une fois dans la base. SSL est un protocole en ligne.
</simpara>
<simpara>
Une fois que le pirate a obtenu l'accès direct à la base de données
(en contournant le serveur web), les données sensibles, stockées dans la
base sont accessibles directement, à moins que les données de la base
ne soient protégées par la base. Chiffrer les données est une bonne
solution pour réduire cette menace, mais très peu de bases de données
offrent ce type de chiffrement.
</simpara>
<simpara>
Le moyen le plus simple pour contourner ce problème est de créer un
logiciel de chiffrement propre, et de l'utiliser dans les scripts PHP.
<acronym>PHP</acronym> peut aider dans cette tâche grâce aux nombreuses extensions
dont il dispose, comme
<link linkend="book.openssl">OpenSSL</link> et
<link linkend="book.sodium">Sodium</link>, qui connaissent un large éventail
de méthodes de chiffrement. Le script PHP va chiffrer les données qui
seront stockées, et les déchiffrer lorsqu'elles seront relues. Voir la
suite pour des exemples d'utilisation de ce chiffrement.
</simpara>
<sect2 xml:id="security.database.storage.hashing">
<title>Hachage</title>
<simpara>
Dans le cas de données vraiment sensibles, si la représentation originale
n'est pas nécessaire (pour affichage, ou comparaison), utiliser un
hachage est une bonne solution. L'exemple classique est le stockage de
mots de passe dans les bases de données, après les avoir passés par
un hachage cryptographique.
</simpara>
<simpara>
Les fonctions <link linkend="ref.password">password</link>
fournissent une bonne façon de hacher les données sensibles et de travailler avec ces empreintes.
</simpara>
<simpara>
La fonction <function>password_hash</function> est utilisée pour hacher une chaîne donnée
en utilisant l'algorithme le plus fort actuellement disponible et la fonction
<function>password_verify</function> vérifie si le mot de passe fourni correspond au
hachage stocké en base de données.
</simpara>
<example>
<title>Hacher un champ mot de passe</title>
<programlisting role="php">
<![CDATA[
<?php
// stockage du hash du mot de passe
$query = sprintf("INSERT INTO users(name,pwd) VALUES('%s','%s');",
pg_escape_string($username),
password_hash($password, PASSWORD_DEFAULT));
$result = pg_query($connection, $query);
// on vérifie si l'utilisateur a soumis le bon mot de passe
$query = sprintf("SELECT pwd FROM users WHERE name='%s';",
pg_escape_string($username));
$row = pg_fetch_assoc(pg_query($connection, $query));
if ($row && password_verify($password, $row['pwd'])) {
echo 'Bonjour, ' . htmlspecialchars($username) . '!';
} else {
echo 'L\'authentification a échoué pour ' . htmlspecialchars($username) . '.';
}
?>
]]>
</programlisting>
</example>
</sect2>
</sect1>
<sect1 xml:id="security.database.sql-injection">
<title>Injection SQL</title>
<simpara>
L'injection SQL est une technique où un attaquant exploite les failles
dans le code d'application responsable de la création de requêtes SQL dynamiques.
L'attaquant peut accéder à des sections privilégiées de l'application,
récupérer toutes les informations de la base de données, altérer des données existantes,
voire exécuter des commandes dangereuses au niveau système sur l'hôte de la base de données.
La vulnérabilité se produit lorsque les développeurs concatènent ou
interpolent une entrée arbitraire dans leurs déclarations SQL.
</simpara>
<para>
<example>
<title>
Séparation des résultats en pages, et créer des superutilisateurs
(PostgreSQL)
</title>
<simpara>
Dans l'exemple suivant, l'entrée de l'utilisateur est directement interpolée dans la
requête SQL, permettant à l'attaquant d'obtenir un compte superutilisateur dans la base de données.
</simpara>
<programlisting role="php">
<![CDATA[
<?php
$offset = $_GET['offset']; // Attention, aucune validation!
$query = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
$result = pg_query($conn, $query);
?>
]]>
</programlisting>
</example>
Un utilisateur normal clique sur les boutons 'suivant' et 'précédent',
qui sont alors placés dans la variable <varname>$offset</varname>,
encodée dans l'<acronym>URL</acronym>. Le script s'attend à ce que la variable
<varname>$offset</varname> soit alors un nombre. Cependant,
il est possible de modifier l'<acronym>URL</acronym> en ajoutant une nouvelle valeur,
au format <acronym>URL</acronym>, comme ceci :
<informalexample>
<programlisting role="sql">
<![CDATA[
0;
insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd)
select 'crack', usesysid, 't','t','crack'
from pg_shadow where usename='postgres';
--
]]>
</programlisting>
</informalexample>
Si cela arrive, le script donnerait un accès super utilisateur à l'attaquant.
Il est à noter que la valeur <literal>0;</literal> fournit un décalage valide à la requête
originale et la termine correctement.
</para>
<note>
<para>
C'est une technique répandue que de forcer l'analyseur SQL à ignorer le
reste de la requête, en utilisant les symboles <literal>--</literal> pour
mettre en commentaires.
</para>
</note>
<para>
Un moyen disponible pour accéder aux mots de passe est de contourner
la recherche de page. Ce que le pirate doit faire, c'est simplement
voir si une variable du formulaire est utilisée dans la requête, et
si elle est mal gérée. Ces variables peuvent avoir été configurées
dans une page précédente pour être utilisées dans les clauses
<literal>WHERE, ORDER BY, LIMIT</literal> et <literal>OFFSET</literal> des
requêtes <literal>SELECT</literal>. Si la base de données supporte
les commandes <literal>UNION</literal>, le pirate peut essayer d'ajouter
une requête entière pour lister les mots de passe dans n'importe quelle
table. Utiliser la technique de stocker uniquement des hachages sécurisés
des mots de passe au lieu des mots de passe eux-mêmes est fortement
recommandé.
<example>
<title>
Liste d'articles ... et quelques mots de passe (n'importe quel serveur de bases de données)
</title>
<programlisting role="php">
<![CDATA[
<?php
$query = "SELECT id, name, inserted, size FROM products
WHERE size = '$size'";
$result = odbc_exec($conn, $query);
?>
]]>
</programlisting>
</example>
La partie statique de la requête, combinée avec une autre
requête <literal>SELECT</literal>, va révéler les mots de passe :
<informalexample>
<programlisting role="sql">
<![CDATA[
'
union select '1', concat(uname||'-'||passwd) as name, '1971-01-01', '0' from usertable;
--
]]>
</programlisting>
</informalexample>
</para>
<para>
Les instructions <literal>UPDATE</literal> et <literal>INSERT</literal> sont également
susceptibles à de telles attaques
<example>
<title>Modifier un mot de passe ... et gain de droits ! (n'importe quel serveur de bases de données)</title>
<programlisting role="php">
<![CDATA[
<?php
$query = "UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';";
?>
]]>
</programlisting>
</example>
Un internaute fourbe peut envoyer une valeur telle que
<literal>' or uid like'%admin%</literal> dans <varname>$uid</varname>
pour modifier le mot de passe utilisateur, ou simplement, utiliser la
variable <varname>$pwd</varname> avec la valeur
<literal>hehehe', trusted=100, admin='yes</literal>
pour obtenir des droits supplémentaires. La requête devient alors :
<informalexample>
<programlisting role="php">
<![CDATA[
<?php
// $uid: ' or uid like '%admin%
$query = "UPDATE usertable SET pwd='...' WHERE uid='' or uid like '%admin%';";
// $pwd: hehehe', trusted=100, admin='yes
$query = "UPDATE usertable SET pwd='hehehe', trusted=100, admin='yes' WHERE
...;";
?>
]]>
</programlisting>
</informalexample>
</para>
<simpara>
Bien qu'il reste évident qu'un attaquant doit posséder au moins
certaines connaissances sur l'architecture de la base de données pour mener une attaque réussie,
obtenir ces informations est souvent très simple. Par exemple,
le code peut faire partie d'un logiciel open-source et être disponible publiquement.
Ces informations peuvent également être divulguées par un code source fermé -
même s'il est codé, obscurci ou compilé - et même par le code de l'application à travers l'affichage de messages d'erreur.
D'autres méthodes comprennent l'utilisation de noms de table et de colonne typiques.
Par exemple, un formulaire de connexion qui utilise une table 'users' avec des noms de colonnes
'id', 'username' et 'password'.
</simpara>
<para>
<example>
<title>Attaque du système d'exploitation de l'hôte de base de données (MSSQL Server)</title>
<simpara>
Un exemple effrayant de la manière dont des commandes de niveau système d'exploitation
peuvent être accessibles sur certains hôtes de base de données.
</simpara>
<programlisting role="php">
<![CDATA[
<?php
$query = "SELECT * FROM products WHERE id LIKE '%$prod%'";
$result = mssql_query($query);
?>
]]>
</programlisting>
</example>
Si le pirate injecte la valeur
<literal>a%' exec master..xp_cmdshell 'net user test testpass /ADD' --</literal>
dans la variable <varname>$prod</varname>, alors la requête
<varname>$query</varname> devient :
<informalexample>
<programlisting role="php">
<![CDATA[
<?php
$query = "SELECT * FROM products
WHERE id LIKE '%a%'
exec master..xp_cmdshell 'net user test testpass /ADD' --%'";
$result = mssql_query($query);
?>
]]>
</programlisting>
</informalexample>
MSSQL Server exécute les requêtes SQL en lot, y compris la commande
d'ajout d'un nouvel utilisateur à la base de données locale. Si cette
application fonctionnait en tant que <literal>sa</literal> et que le
service MSSQLSERVER disposait d'un niveau de droits suffisant, le pirate
disposerait désormais d'un compte avec accès au serveur.
</para>
<note>
<para>
Certains exemples ci-dessus sont spécifiques à certains serveurs de
bases de données, mais cela n'empêche pas des attaques similaires d'être
possibles sur d'autres produits. La base de données sera alors
vulnérable d'une autre manière.
</para>
</note>
<para>
<mediaobject>
<alt>Un exemple amusant concernant l'injection SQL</alt>
<imageobject>
<imagedata fileref="en/security/figures/xkcd-bobby-tables.png" format="PNG"/>
</imageobject>
<caption>
<simpara>
Image de <link xlink:href="&url.xkcd;327">xkcd</link>
</simpara>
</caption>
</mediaobject>
</para>
<sect2 xml:id="security.database.avoiding">
<title>Techniques de contournement</title>
<para>
La méthode recommandée pour éviter les injections SQL est de lier toutes les données
via des requêtes préparées. L'utilisation de requêtes paramétrées n'est pas suffisante
pour éviter complètement les injections SQL, mais c'est le moyen le plus facile et le plus sûr
de fournir une entrée aux instructions SQL. Toutes les valeurs littérales dynamiques dans les clauses
<literal>WHERE</literal>, <literal>SET</literal> et <literal>VALUES</literal> doivent être remplacées
par des espaces réservés. Les données réelles seront liées pendant
l'exécution et envoyées séparément de la commande SQL.
</para>
<para>
La liaison de paramètres ne peut être utilisée que pour les données. Les autres parties dynamiques de la requête SQL doivent être filtrées contre une liste connue de valeurs autorisées.
</para>
<para>
<example>
<title>Éviter les injections SQL en utilisant des requêtes préparées PDO</title>
<programlisting role="php">
<![CDATA[
<?php
// La partie SQL dynamique est validée par rapport aux valeurs attendues
$sortingOrder = $_GET['sortingOrder'] === 'DESC' ? 'DESC' : 'ASC';
$productId = $_GET['productId'];
// Le SQL est préparé avec un espace réservé
$stmt = $pdo->prepare("SELECT * FROM products WHERE id LIKE ? ORDER BY price {$sortingOrder}");
// La valeur est fournie avec des caractères génériques LIKE
$stmt->execute(["%{$productId}%"]);
?>
]]>
</programlisting>
</example>
</para>
<simpara>
Les instructions préparées sont fournies
<link linkend="pdo.prepared-statements">par PDO</link>,
<link linkend="mysqli.quickstart.prepared-statements">par MySQLi</link>,
et par d'autres bibliothèques de bases de données.
</simpara>
<simpara>
Les attaques par injection SQL sont principalement basées sur l'exploitation du code qui n'a pas été écrit
en tenant compte de la sécurité. Il ne faut jamais faire confiance à une entrée, en particulier
côté client, même si elle provient d'une boîte de sélection,
d'un champ de saisie masqué ou d'un cookie. Le premier exemple montre qu'une telle requête simple peut causer des désastres.
</simpara>
<para>
Une stratégie de défense en profondeur implique plusieurs bonnes pratiques de codage :
<itemizedlist>
<listitem>
<simpara>
Il ne faut jamais se connecter à la base de données en tant que superutilisateur ou en tant que propriétaire de la base de données.
Il convient de toujours utiliser des utilisateurs personnalisés avec des privilèges minimaux.
</simpara>
</listitem>
<listitem>
<simpara>
Il faut vérifier si l'entrée donnée a le type de données attendu. <acronym>PHP</acronym> a
une large gamme de fonctions de validation d'entrée, des plus simples
trouvées dans <link linkend="ref.var">Fonctions de variables</link> et
dans <link linkend="ref.ctype">Fonctions de type de caractères</link>
(par exemple <function>is_numeric</function>, <function>ctype_digit</function>
respectivement) et jusqu'à la
prise en charge des <link linkend="ref.pcre">Expressions régulières compatibles avec Perl</link>.
</simpara>
</listitem>
<listitem>
<simpara>
Si l'application s'attend à une entrée numérique, envisagez de vérifier les données
avec <function>ctype_digit</function>, de changer silencieusement son type
en utilisant <function>settype</function>, ou d'utiliser sa représentation numérique
avec <function>sprintf</function>.
</simpara>
</listitem>
<listitem>
<simpara>
Si la couche de base de données ne prend pas en charge la liaison de variables, alors
mettez chaque valeur fournie par l'utilisateur non numérique entre guillemets avec la fonction d'échappement de chaîne spécifique à la base de données (par exemple
<function>mysql_real_escape_string</function>,
<function>sqlite_escape_string</function>, etc.).
Les fonctions génériques comme <function>addslashes</function> ne sont utiles que dans un environnement très spécifique (par exemple MySQL dans un ensemble de caractères à octets uniques avec <varname>NO_BACKSLASH_ESCAPES</varname> désactivé), il est donc
préférable de les éviter.
</simpara>
</listitem>
<listitem>
<simpara>
Il est particulièrement important de ne pas divulguer d'informations spécifiques à la base de données,
en particulier sur le schéma, que ce soit de manière légale ou illégale.
Se référer également à <link linkend="security.errors">Rapport d'erreurs</link> et
<link linkend="ref.errorfunc">Fonctions de gestion et de journalisation des erreurs</link>.
</simpara>
</listitem>
</itemizedlist>
</para>
<simpara>
À côté de ces conseils, il est recommandé d'enregistrer les requêtes, soit
dans les scripts, soit dans la base elle-même, si elle le supporte.
Évidemment, cet enregistrement ne sera pas capable d'empêcher une attaque,
mais permettra de retrouver la requête qui a fauté. L'historique
n'est pas très utile par lui-même mais au niveau des informations qu'il
contient. Plus il y a de détails, mieux c'est.
</simpara>
</sect2>
</sect1>
</chapter>
<!-- Keep this comment at the end of the file
Local variables:
mode: sgml
sgml-omittag:t
sgml-shorttag:t
sgml-minimize-attributes:nil
sgml-always-quote-attributes:t
sgml-indent-step:1
sgml-indent-data:t
indent-tabs-mode:nil
sgml-parent-document:nil
sgml-default-dtd-file:"~/.phpdoc/manual.ced"
sgml-exposed-tags:nil
sgml-local-catalogs:nil
sgml-local-ecat-files:nil
End:
vim600: syn=xml fen fdm=syntax fdl=2 si
vim: et tw=78 syn=sgml
vi: ts=1 sw=1
-->