Risques pour la sécurité
Les bases de données contiennent souvent des données sensibles et permettent d'effectuer des opérations dangereuses. Pour travailler en toute sécurité avec Nette Database, les aspects clés sont les suivants :
- Comprendre la différence entre une API sécurisée et une API non sécurisée
- Utiliser des requêtes paramétrées
- Valider correctement les données d'entrée
Qu'est-ce que l'injection SQL ?
L'injection SQL est le risque de sécurité le plus sérieux lorsque l'on travaille avec des bases de données. Elle se produit lorsque l'entrée non filtrée de l'utilisateur devient une partie d'une requête SQL. Un attaquant peut insérer ses propres commandes SQL et ainsi :
- extraire des données non autorisées
- modifier ou supprimer des données dans la base de données
- Contourner l'authentification
// ❌ CODE DANGEREUX - vulnérable à l'injection SQL
$database->query("SELECT * FROM users WHERE name = '$_GET[name]'");
// Un attaquant pourrait entrer une valeur comme: ' OR '1'='1
// La requête résultante serait: SELECT * FROM users WHERE name = '' OR '1'='1'
// Ce qui renvoie tous les utilisateurs
Il en va de même pour l'explorateur de bases de données :
// ❌ CODE DANGEREUX - vulnérable à l'injection SQL
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");
Requêtes paramétrées sécurisées
Les requêtes paramétrées constituent la défense fondamentale contre les injections SQL. Nette Database propose plusieurs façons de les utiliser.
La manière la plus simple est d'utiliser des marques d'espacement pour les questions :
// ✅ Requête paramétrée sécurisée
$database->query('SELECT * FROM users WHERE name = ?', $name);
// ✅ Condition de sécurité dans l'explorateur
$table->where('name = ?', $name);
Ceci s'applique à toutes les autres méthodes de l'explorateur de bases de données qui permettent d'insérer des expressions avec des points d'interrogation et des paramètres.
Pour les clauses INSERT
, UPDATE
, ou WHERE
, vous pouvez transmettre des valeurs dans un
tableau :
// ✅ Secure INSERT
$database->query('INSERT INTO users', [
'name' => $name,
'email' => $email,
]);
// ✅ Secure INSERT dans Explorer
$table->insert([
'name' => $name,
'email' => $email,
]);
Validation de la valeur des paramètres
Les requêtes paramétrées sont la pierre angulaire d'un travail sécurisé sur les bases de données. Toutefois, les valeurs qui leur sont transmises doivent être soumises à plusieurs niveaux de validation :
Vérification du type
Il est essentiel de s'assurer que le type de données des paramètres est correct – il s'agit d'une condition nécessaire pour utiliser la base de données Nette en toute sécurité. La base de données suppose que toutes les données d'entrée ont le type de données correct correspondant à la colonne.
Par exemple, si $name
dans les exemples précédents devenait inopinément un tableau au lieu d'une chaîne, Nette
Database essaierait d'insérer tous ses éléments dans la requête SQL, ce qui provoquerait une erreur. Par conséquent, ne
jamais utiliser des données non validées provenant de $_GET
, $_POST
, ou $_COOKIE
directement dans les requêtes de la base de données.
Validation du format
Le deuxième niveau vérifie le format des données, par exemple en s'assurant que les chaînes de caractères sont codées en UTF-8 et que leur longueur correspond à la définition de la colonne, ou en vérifiant que les valeurs numériques se situent dans la plage autorisée pour le type de données de la colonne.
À ce niveau, vous pouvez vous fier en partie à la base de données elle-même : de nombreuses bases de données rejettent les données non valides. Toutefois, le comportement peut varier : certaines tronquent silencieusement les longues chaînes de caractères ou coupent les nombres qui ne sont pas dans la plage autorisée.
Validation spécifique à un domaine
Le troisième niveau implique des contrôles logiques spécifiques à votre application. Il s'agit, par exemple, de vérifier que les valeurs des cases à cocher correspondent aux options disponibles, que les nombres se situent dans une fourchette attendue (par exemple, âge de 0 à 150 ans) ou que les relations entre les valeurs sont logiques.
Méthodes de validation recommandées
- Utilisez les formulaires Nette, qui gèrent automatiquement la validation de toutes les entrées.
- Utilisez des présentateurs et déclarez les types de données des
paramètres dans les méthodes
action*()
etrender*()
. - Ou implémentez une couche de validation personnalisée en utilisant des outils PHP standard comme
filter_var()
.
Travailler en toute sécurité avec les colonnes
Dans la section précédente, nous avons vu comment valider correctement les valeurs des paramètres. Cependant, lors de l'utilisation de tableaux dans des requêtes SQL, il convient d'accorder la même attention à leurs clés.
// ❌ CODE DANGEREUX - les clés des tableaux ne sont pas assainies
$database->query('INSERT INTO users', $_POST);
Pour les commandes INSERT et UPDATE, il s'agit d'une faille de sécurité majeure : un pirate peut insérer ou modifier
n'importe quelle colonne de la base de données. Il peut, par exemple, définir is_admin = 1
ou insérer des données
arbitraires dans des colonnes sensibles (vulnérabilité d'affectation de masse).
Dans les conditions WHERE, c'est encore plus dangereux car elles peuvent contenir des opérateurs :
// CODE DANGEREUX - les clés des tableaux ne sont pas nettoyées
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// exécute la requête WHERE (`salary` > 100000)
Un pirate peut utiliser cette approche pour découvrir systématiquement les salaires des employés. Il peut commencer par demander les salaires supérieurs à 100 000, puis inférieurs à 50 000, et en réduisant progressivement la fourchette, il peut révéler les salaires approximatifs de tous les employés. Ce type d'attaque est appelé énumération SQL.
Les méthodes where()
et whereOr()
sont encore plus souples et prennent en charge les expressions SQL, y compris
les opérateurs et les fonctions, dans les clés et les valeurs. Cela permet à un pirate d'effectuer des injections SQL
complexes :
// ❌ CODE DANGEREUX - l'attaquant peut insérer son propre code SQL
$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1'];
$table->where($_POST);
// exécute la requête WHERE (0) UNION SELECT nom, salaire FROM utilisateurs WHERE (1)
Cette attaque termine la condition originale avec 0)
, ajoute son propre SELECT
en utilisant
UNION
pour obtenir des données sensibles de la table users
, et termine avec une requête syntaxiquement
correcte en utilisant WHERE (1)
.
Liste blanche des colonnes
Pour travailler en toute sécurité avec les noms de colonnes, vous avez besoin d'un mécanisme qui garantit que les utilisateurs ne peuvent interagir qu'avec les colonnes autorisées et ne peuvent pas ajouter les leurs. Tenter de détecter et de bloquer les noms de colonnes dangereux (blacklistage) n'est pas fiable : un pirate peut toujours trouver une nouvelle façon d'écrire un nom de colonne dangereux que vous n'avez pas anticipé.
Il est donc beaucoup plus sûr d'inverser la logique et de définir une liste explicite de colonnes autorisées (liste blanche) :
// Colonnes que l'utilisateur est autorisé à modifier
$allowedColumns = ['name', 'email', 'active'];
// Supprimer toutes les colonnes non autorisées de l'entrée
$filteredData = array_intersect_key($userData, array_flip($allowedColumns));
// Maintenant utilisable en toute sécurité dans les requêtes, telles que:
$database->query('INSERT INTO users', $filteredData);
$table->update($filteredData);
$table->where($filteredData);
Identifiants dynamiques
Pour les noms de tables et de colonnes dynamiques, utilisez l'espace réservé ?name
. Cela permet de s'assurer que
les identificateurs sont correctement échappés conformément à la syntaxe de la base de données concernée (par exemple, en
utilisant des barres obliques dans MySQL) :
// ✅ Utilisation sûre des identificateurs de confiance
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// Résultat dans MySQL: SELECT `nom` FROM `users`
Important : utilisez le symbole ?name
uniquement pour les valeurs de confiance définies dans le code de
l'application. Pour les valeurs fournies par l'utilisateur, utilisez à nouveau une liste
blanche. Dans le cas contraire, vous risquez des failles de sécurité :
// DANGEREUX - ne jamais utiliser les données de l'utilisateur
$database->query('SELECT ?name FROM users', $_GET['column']);