CascadiaPHP 2024

Injection SQL

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.

Exemple #1 Séparation des résultats en pages, et créer des administrateurs (PostgreSQL et MySQL)

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.

<?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);
?>
Un utilisateur normal clique sur les boutons 'suivant' et 'précédent', qui sont alors placés dans la variable $offset, encodée dans l'URL. Le script s'attend à ce que la variable $offset soit alors un nombre. Cependant, il est possible de modifier l'URL en ajoutant une nouvelle valeur, au format URL, comme ceci :

Exemple #2 Exemple d'injection SQL

0;
insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd)
    select 'crack', usesysid, 't','t','crack'
    from pg_shadow where usename='postgres';
--
Si cela arrive, le script donnerait un accès super utilisateur à l'attaquant. Notez que la valeur 0; sert à terminer la requête originale et la terminer correctement.

Note:

C'est une technique répandue que de forcer l'analyseur SQL à ignorer le reste de la requête, en utilisant les symboles -- pour mettre en commentaires.

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 WHERE, ORDER BY, LIMIT et OFFSET des requêtes SELECT. Si votre base de données supporte les commandes UNION, 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é.

Exemple #3 Liste d'articles ... et ajout de mot de passe

<?php
$query
= "SELECT id, name, inserted, size FROM products
WHERE size = '
$size'";
$result = odbc_exec($conn, $query);
?>
La partie statique de la requête, combinée avec une autre requête SELECT, va révéler les mots de passe :

Exemple #4 Révélation des mots de passe

<?php
'
union select '1', concat(uname||'-'||passwd) as name, '1971-01-01', '0' from usertable;
--
?>

Les instructions UPDATE et INSERT sont également susceptibles à de telles attaques

Exemple #5 Modifier un mot de passe ... et gain de droits!

<?php
$query
= "UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';";
?>
Un internaute fourbe peut envoyer une valeur telle que ' or uid like'%admin% dans $uid pour modifier le mot de passe utilisateur, ou simplement, utiliser la variable $pwd avec la valeur hehehe', trusted=100, admin='yes pour obtenir des droits supplémentaires. La requête devient alors :

Exemple #6 Une requête et son injection

<?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
...;"
;

?>

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 votre propre code à 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'.

Exemple #7 Attaque d'un serveur de bases de données (MSSQL Server)

Un exemple effrayant de la manière dont des commandes de niveau systèmes d'exploitation peuvent être accessibles sur certains hôtes de base de données.

<?php
$query
= "SELECT * FROM products WHERE id LIKE '%$prod%'";
$result = mssql_query($query);
?>
Si le pirate injecte la valeur a%' exec master..xp_cmdshell 'net user test testpass /ADD' -- dans la variable $prod, alors la requête $query devient :

Exemple #8 Attaque d'un serveur de base de données (MSSQL Server) - 2

<?php
$query
= "SELECT * FROM products
WHERE id LIKE '%a%'
exec master..xp_cmdshell 'net user test testpass /ADD' --%'"
;
$result = mssql_query($query);
?>
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 sa et que le service MSSQLSERVER disposait de niveau de droits suffisant, le pirate dispose désormais d'un compte avec accès au serveur.

Note:

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. Votre base de données sera alors vulnérable d'une autre manière.

Un exemple amusant concernant l'injection SQL

Image de » xkcd

Techniques de contournement

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 littérales de données dynamiques dans les clauses WHERE, SET et VALUES 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.

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.

Exemple #9 Éviter les injections SQL en utilisant des requêtes préparées PDO

<?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}%"]);
?>

Les instructions préparées sont fournies par PDO, par MySQLi, et par d'autres bibliothèques de bases de données.

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é. Ne faites jamais 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.

Une stratégie de défense en profondeur implique plusieurs bonnes pratiques de codage :

  • Ne vous connectez jamais à la base de données en tant que superutilisateur ou en tant que propriétaire de la base de données. Utilisez toujours des utilisateurs personnalisés avec des privilèges minimaux.
  • Vérifiez si l'entrée donnée a le type de données attendu. PHP a une large gamme de fonctions de validation d'entrée, des plus simples trouvées dans Fonctions de variables et dans Fonctions de type de caractères (par exemple is_numeric(), ctype_digit() respectivement) et jusqu'à la prise en charge des Expressions régulières compatibles avec Perl.
  • Si l'application s'attend à une entrée numérique, envisagez de vérifier les données avec ctype_digit(), de changer silencieusement son type en utilisant settype(), ou d'utiliser sa représentation numérique avec sprintf().
  • 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 mysql_real_escape_string(), sqlite_escape_string(), etc.). Les fonctions génériques comme addslashes() ne sont utiles que dans un environnement très spécifique (par exemple MySQL dans un ensemble de caractères à octets uniques avec NO_BACKSLASH_ESCAPES désactivé), il est donc préférable de les éviter.
  • 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. Consultez également Signalement d'erreurs et Fonctions de gestion et de journalisation des erreurs.

À côté de ces conseils, il est recommandé d'enregistrer vos requêtes, soit dans vos 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 vous 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 vous avez de détails, mieux c'est.

add a note

User Contributed Notes 1 note

up
42
Richard dot Corfield at gmail dot com
12 years ago
The best way has got to be parameterised queries. Then it doesn't matter what the user types in the data goes to the database as a value.

A quick search online shows some possibilities in PHP which is great! Even on this site - http://php.net/manual/en/pdo.prepared-statements.php
which also gives the reasons this is good both for security and performance.
To Top