Risques de sécurité

La base de données contient souvent des données sensibles et permet d'effectuer des opérations dangereuses. Pour travailler en toute sécurité avec Nette Database, il est crucial de :

  • Comprendre la différence entre une API sécurisée et 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 grave lors du travail avec une base de données. Elle se produit lorsque l'entrée non traitée d'un utilisateur fait partie d'une requête SQL. Un attaquant peut insérer ses propres commandes SQL et ainsi :

  • Obtenir un accès non autorisé aux donné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]'");

// L'attaquant peut entrer une valeur comme : ' OR '1'='1
// La requête résultante sera : SELECT * FROM users WHERE name = '' OR '1'='1'
// Ce qui renvoie tous les utilisateurs

Il en va de même pour Database Explorer :

// ❌ CODE DANGEREUX - vulnérable à l'injection SQL
$table->where('name = ' . $_GET['name']);
$table->where("name = '$_GET[name]'");

Requêtes paramétrées

La défense de base contre l'injection SQL consiste à utiliser des requêtes paramétrées. Nette Database offre plusieurs façons de les utiliser.

La méthode la plus simple consiste à utiliser des points d'interrogation comme placeholders :

// ✅ Requête paramétrée sécurisée
$database->query('SELECT * FROM users WHERE name = ?', $name);

// ✅ Condition sécurisée dans l'Explorer
$table->where('name = ?', $name);

Cela s'applique à toutes les autres méthodes de Database Explorer qui permettent d'insérer des expressions avec des points d'interrogation et des paramètres.

Pour les commandes INSERT, UPDATE ou la clause WHERE, nous pouvons passer les valeurs dans un tableau :

// ✅ INSERT sécurisé
$database->query('INSERT INTO users', [
	'name' => $name,
	'email' => $email,
]);

// ✅ INSERT sécurisé dans l'Explorer
$table->insert([
	'name' => $name,
	'email' => $email,
]);

Validation des valeurs des paramètres

Les requêtes paramétrées sont la pierre angulaire d'un travail sécurisé avec la base de données. Cependant, les valeurs que nous y insérons doivent passer par plusieurs niveaux de contrôles :

Contrôle de type

Le plus important est d'assurer le type de données correct des paramètres – c'est une condition nécessaire pour une utilisation sécurisée de Nette Database. La base de données suppose que toutes les données d'entrée ont le type de données correct correspondant à la colonne donnée.

Par exemple, si $name dans les exemples précédents était de manière inattendue un tableau au lieu d'une chaîne, Nette Database tenterait d'insérer tous ses éléments dans la requête SQL, ce qui entraînerait une erreur. Par conséquent, n'utilisez jamais de données non validées de $_GET, $_POST ou $_COOKIE directement dans les requêtes de base de données.

Contrôle de format

Au deuxième niveau, nous vérifions le format des données – par exemple, si les chaînes sont en encodage UTF-8 et si leur longueur correspond à la définition de la colonne, ou si les valeurs numériques sont dans la plage autorisée pour le type de données de la colonne donnée.

Pour ce niveau de validation, nous pouvons également compter en partie sur la base de données elle-même – de nombreuses bases de données refuseront les données non valides. Cependant, le comportement peut varier, certaines peuvent tronquer silencieusement les longues chaînes ou couper les nombres hors plage.

Contrôle de domaine

Le troisième niveau représente les contrôles logiques spécifiques à votre application. Par exemple, vérifier que les valeurs des listes déroulantes correspondent aux options proposées, que les nombres sont dans la plage attendue (par exemple, âge 0–150 ans) ou que les dépendances mutuelles entre les valeurs ont un sens.

Méthodes de validation recommandées

  • Utilisez Nette Forms, qui assurent automatiquement la validation correcte de toutes les entrées
  • Utilisez les Presenters et spécifiez les types de données pour les paramètres dans les méthodes action*() et render*()
  • Ou implémentez votre propre couche de validation 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 montré comment valider correctement les valeurs des paramètres. Cependant, lors de l'utilisation de tableaux dans les requêtes SQL, nous devons accorder la même attention à leurs clés.

// ❌ CODE DANGEREUX - les clés du tableau ne sont pas traitées
$database->query('INSERT INTO users', $_POST);

Pour les commandes INSERT et UPDATE, il s'agit d'une faille de sécurité critique – un attaquant peut insérer ou modifier n'importe quelle colonne dans la base de données. Il pourrait, par exemple, définir is_admin = 1 ou insérer des données arbitraires dans des colonnes sensibles (vulnérabilité dite Mass Assignment).

Dans les conditions WHERE, c'est encore plus dangereux, car elles peuvent contenir des opérateurs :

// ❌ CODE DANGEREUX - les clés du tableau ne sont pas traitées
$_POST['salary >'] = 100000;
$database->query('SELECT * FROM users WHERE', $_POST);
// exécute la requête WHERE (`salary` > 100000)

Un attaquant peut utiliser cette approche pour découvrir systématiquement les salaires des employés. Il commence, par exemple, par une requête sur les salaires supérieurs à 100 000, puis inférieurs à 50 000, et en réduisant progressivement la plage, 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 beaucoup plus flexibles et supportent dans les clés et les valeurs des expressions SQL incluant des opérateurs et des fonctions. Cela donne à l'attaquant la possibilité d'effectuer une injection SQL :

// ❌ CODE DANGEREUX - l'attaquant peut injecter son propre SQL
$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1'];
$table->where($_POST);
// exécute la requête WHERE (0) UNION SELECT name, salary FROM users WHERE (1)

Cette attaque termine la condition d'origine avec 0), ajoute son propre SELECT en utilisant UNION pour obtenir des données sensibles de la table users et ferme la requête syntaxiquement correcte avec WHERE (1).

Liste blanche de colonnes

Pour travailler en toute sécurité avec les noms de colonnes, nous avons besoin d'un mécanisme qui garantit que l'utilisateur ne peut travailler qu'avec les colonnes autorisées et ne peut pas ajouter les siennes. Nous pourrions essayer de détecter et de bloquer les noms de colonnes dangereux (liste noire), mais cette approche n'est pas fiable – un attaquant peut toujours trouver une nouvelle façon d'écrire un nom de colonne dangereux que nous n'avions pas prévu.

Par conséquent, il est beaucoup plus sûr d'inverser la logique et de définir une liste explicite des colonnes autorisées (liste blanche) :

// Colonnes que l'utilisateur peut 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, nous pouvons l'utiliser 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 le placeholder ?name. Cela garantit un échappement correct des identifiants selon la syntaxe de la base de données donnée (par exemple, en utilisant des backticks en MySQL) :

// ✅ Utilisation sécurisée d'identifiants fiables
$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name', $column, $table);
// Résultat dans MySQL : SELECT `name` FROM `users`

Important : utilisez le symbole ?name uniquement pour les valeurs fiables définies dans le code de l'application. Pour les valeurs provenant de l'utilisateur, utilisez à nouveau la liste blanche. Sinon, vous vous exposez à des risques de sécurité :

// ❌ DANGEREUX - n'utilisez jamais l'entrée utilisateur
$database->query('SELECT ?name FROM users', $_GET['column']);
version: 4.0