Question Comment puis-je empêcher l'injection SQL en PHP?


Si l'entrée utilisateur est insérée sans modification dans une requête SQL, l'application devient vulnérable à Injection SQL, comme dans l'exemple suivant:

$unsafe_variable = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

C'est parce que l'utilisateur peut entrer quelque chose comme value'); DROP TABLE table;--et la requête devient:

INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')

Que peut-on faire pour empêcher cela de se produire?


2781


origine


Réponses:


Utilisez des instructions préparées et des requêtes paramétrées. Ce sont des instructions SQL qui sont envoyées et analysées par le serveur de base de données séparément de tous les paramètres. De cette façon, il est impossible pour un attaquant d'injecter un code SQL malveillant.

Vous avez essentiellement deux options pour y parvenir:

  1. En utilisant AOP (pour tout pilote de base de données pris en charge):

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute(array('name' => $name));
    
    foreach ($stmt as $row) {
        // do something with $row
    }
    
  2. En utilisant MySQLi (pour MySQL):

    $stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
    $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
    
    $stmt->execute();
    
    $result = $stmt->get_result();
    while ($row = $result->fetch_assoc()) {
        // do something with $row
    }
    

Si vous vous connectez à une base de données autre que MySQL, il existe une seconde option spécifique au pilote à laquelle vous pouvez vous référer (par ex. pg_prepare() et pg_execute() pour PostgreSQL). PDO est l'option universelle.

Configuration correcte de la connexion

Notez que lorsque vous utilisez PDO accéder à une base de données MySQL réal déclarations préparées sont non utilisé par défaut. Pour résoudre ce problème, vous devez désactiver l'émulation des instructions préparées. Un exemple de création d'une connexion à l'aide de PDO est:

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

Dans l'exemple ci-dessus, le mode d'erreur n'est pas strictement nécessaire, mais il est conseillé de l'ajouter. De cette façon, le script ne s'arrêtera pas avec un Fatal Error quand quelque chose ne va pas. Et cela donne au développeur l'opportunité de catch toute erreur (s) qui sont thrown comme PDOExceptions.

Quel est obligatoireCependant, c'est le premier setAttribute() ligne, qui indique à PDO de désactiver les instructions préparées émulées et l'utilisation réal déclarations préparées. Cela permet de s'assurer que l'instruction et les valeurs ne sont pas analysées par PHP avant de l'envoyer au serveur MySQL (ce qui ne donne aucun risque à un attaquant d'injecter un code SQL malveillant).

Bien que vous puissiez définir le charset dans les options du constructeur, il est important de noter que les anciennes versions de PHP (<5.3.6) ignoré silencieusement le paramètre charset dans le DSN.

Explication

Qu'est-ce qui se passe est que l'instruction SQL que vous passez à prepare est analysé et compilé par le serveur de base de données. En spécifiant des paramètres (soit un ? ou un paramètre nommé comme :name dans l'exemple ci-dessus), vous indiquez au moteur de la base de données où vous voulez filtrer. Ensuite, quand vous appelez execute, l'instruction préparée est combinée avec les valeurs de paramètre que vous spécifiez.

L'important ici est que les valeurs des paramètres sont combinées avec l'instruction compilée, pas une chaîne SQL. L'injection SQL fonctionne en incitant le script à inclure des chaînes malveillantes lorsqu'il crée du code SQL à envoyer à la base de données. Donc, en envoyant le SQL réel séparément des paramètres, vous limitez le risque de vous retrouver avec quelque chose que vous n'aviez pas prévu. Tous les paramètres que vous envoyez lors de l'utilisation d'une instruction préparée seront simplement traités comme des chaînes (bien que le moteur de base de données puisse faire une certaine optimisation, les paramètres peuvent bien sûr finir par être des nombres). Dans l'exemple ci-dessus, si le $namevariable contient 'Sarah'; DELETE FROM employees le résultat serait simplement une recherche de la chaîne "'Sarah'; DELETE FROM employees"et vous ne finirez pas avec une table vide.

Un autre avantage de l'utilisation des instructions préparées est que si vous exécutez la même instruction plusieurs fois dans la même session, elle ne sera analysée et compilée qu'une seule fois, ce qui vous donnera des gains de vitesse.

Oh, et puisque vous avez demandé comment faire pour un insert, voici un exemple (en utilisant PDO):

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');

$preparedStatement->execute(array('column' => $unsafeValue));

Les instructions préparées peuvent-elles être utilisées pour les requêtes dynamiques?

Bien que vous puissiez toujours utiliser des instructions préparées pour les paramètres de requête, la structure de la requête dynamique elle-même ne peut pas être paramétrée et certaines fonctions de requête ne peuvent pas être paramétrées.

Pour ces scénarios spécifiques, la meilleure chose à faire est d'utiliser un filtre de liste blanche qui limite les valeurs possibles.

// Value whitelist
// $dir can only be 'DESC' otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}

7938



Attention:   L'exemple de code de cette réponse (comme l'exemple de code de la question) utilise PHP mysql extension, qui était obsolète en PHP 5.5.0 et entièrement supprimée en PHP 7.0.0.

Si vous utilisez une version récente de PHP, le mysql_real_escape_string l'option décrite ci-dessous ne sera plus disponible mysqli::escape_string est un équivalent moderne). Ces jours-ci mysql_real_escape_string option n'aurait de sens que pour du code existant sur une ancienne version de PHP.


Vous avez deux options - échapper les caractères spéciaux dans votre unsafe_variableou en utilisant une requête paramétrée. Les deux vous protéger de l'injection SQL. La requête paramétrée est considérée comme la meilleure pratique, mais il faudra passer à une nouvelle extension mysql en PHP avant de pouvoir l'utiliser.

Nous couvrirons la chaîne d'impact inférieure qui s'échappe en premier.

//Connect

$unsafe_variable = $_POST["user-input"];
$safe_variable = mysql_real_escape_string($unsafe_variable);

mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

//Disconnect

Voir aussi, les détails de la mysql_real_escape_string fonction.

Pour utiliser la requête paramétrée, vous devez utiliser MySQLi plûtot que le MySQL les fonctions. Pour réécrire votre exemple, nous aurions besoin de quelque chose comme ce qui suit.

<?php
    $mysqli = new mysqli("server", "username", "password", "database_name");

    // TODO - Check that connection was successful.

    $unsafe_variable = $_POST["user-input"];

    $stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");

    // TODO check that $stmt creation succeeded

    // "s" means the database expects a string
    $stmt->bind_param("s", $unsafe_variable);

    $stmt->execute();

    $stmt->close();

    $mysqli->close();
?>

La fonction clé que vous aurez envie de lire là-bas serait mysqli::prepare.

Aussi, comme d'autres l'ont suggéré, vous trouverez peut-être utile / plus facile d'intensifier une couche d'abstraction avec quelque chose comme AOP.

Veuillez noter que le cas que vous avez posé est relativement simple et que des cas plus complexes peuvent nécessiter des approches plus complexes. En particulier:

  • Si vous voulez modifier la structure du SQL en fonction de l'entrée de l'utilisateur, les requêtes paramétrées ne vont pas aider, et l'échappement requis n'est pas couvert par mysql_real_escape_string. Dans ce cas, il vaudrait mieux passer la saisie de l'utilisateur à travers une liste blanche pour s'assurer que seules les valeurs 'sûres' sont autorisées.
  • Si vous utilisez des entiers de l'entrée de l'utilisateur dans une condition et prenez la mysql_real_escape_string approche, vous allez souffrir du problème décrit par Polynôme dans les commentaires ci-dessous. Ce cas est plus délicat car les entiers ne seraient pas entourés de guillemets, vous pouvez donc traiter en validant que l'entrée de l'utilisateur ne contient que des chiffres.
  • Il y a probablement d'autres cas que je ne connais pas. Vous pourriez trouver ce est une ressource utile sur certains des problèmes les plus subtils que vous pouvez rencontrer.

1509



Chaque réponse ici couvre seulement une partie du problème.
En fait, il y a quatre différentes parties de requête que nous pouvons ajouter dynamiquement:

  • un string
  • un numéro
  • un identifiant
  • un mot-clé de syntaxe.

et les déclarations préparées couvre seulement 2 d'entre eux

Mais parfois, nous devons rendre notre requête encore plus dynamique en ajoutant des opérateurs ou des identifiants.
Nous aurons donc besoin de différentes techniques de protection.

En général, une telle approche de protection est basée sur liste blanche. Dans ce cas, chaque paramètre dynamique doit être codé en dur dans votre script et choisi dans cet ensemble.
Par exemple, pour faire un ordre dynamique:

$orders  = array("name","price","qty"); //field names
$key     = array_search($_GET['sort'],$orders)); // see if we have such a name
$orderby = $orders[$key]; //if not, first one will be set automatically. smart enuf :)
$query   = "SELECT * FROM `table` ORDER BY $orderby"; //value is safe

Cependant, il existe un autre moyen de sécuriser les identifiants: l'échappement. Tant que vous avez un identifiant cité, vous pouvez échapper les backticks à l'intérieur en les doublant.

Comme une étape supplémentaire, nous pouvons emprunter une idée vraiment géniale d'utiliser un espace réservé (un proxy pour représenter la valeur réelle dans la requête) à partir des instructions préparées et inventer un espace réservé d'un autre type - un espace réservé d'identificateur.

Donc, pour faire la longue histoire courte: c'est un espace réservé, ne pas Affirmation préparée peut être considéré comme une balle d'argent.

Ainsi, une recommandation générale peut être formulée comme
Tant que vous ajoutez des parties dynamiques à la requête en utilisant des espaces réservés (et bien sûr ces espaces réservés correctement traités), vous pouvez être sûr que votre requête est sûre.

Cependant, il existe un problème avec les mots-clés de syntaxe SQL (tels que AND, DESC et tel) mais la liste blanche semble la seule approche dans ce cas.

Mettre à jour

Bien qu'il existe un accord général sur les meilleures pratiques en matière de protection par injection SQL, il existe encore beaucoup de mauvaises pratiques. Et certains d'entre eux sont trop profondément enracinés dans l'esprit des utilisateurs de PHP. Par exemple, sur cette même page il y a (bien que invisible pour la plupart des visiteurs) plus de 80 réponses supprimées - tous retirés par la communauté en raison de la mauvaise qualité ou de la promotion de pratiques mauvaises et périmées. Pire encore, certaines des mauvaises réponses ne sont pas supprimées mais plutôt prospères.

Par exemple, là (1)  sont (2)  encore (3)  beaucoup (4)  réponses (5), comprenant la deuxième réponse la plus mise à jour vous suggérant une ficelle manuelle - une approche obsolète qui s'est révélée peu sûre.

Ou il y a une réponse légèrement meilleure qui suggère juste une autre méthode de formatage de chaîne et même s'en vante comme la panacée ultime. Bien sûr, ce n'est pas le cas. Cette méthode n'est pas meilleure que la mise en forme régulière mais elle conserve tous ses inconvénients: elle s'applique uniquement aux chaînes de caractères et, comme toute autre mise en forme manuelle, elle est essentiellement facultative, non obligatoire, sujette à erreur humaine.

Je pense que tout cela à cause d'une très vieille superstition, soutenue par de telles autorités comme OWASP ou Manuel PHP, qui proclame l'égalité entre tout ce qui "s'échappe" et la protection contre les injections SQL.

Peu importe ce que PHP manuel dit depuis des siècles, *_escape_string en aucun cas rend les données en toute sécurité et n'a jamais été destiné à. En plus d'être inutile pour toute partie SQL autre que la chaîne, l'échappement manuel est erroné car il est manuel comme opposé à automatisé.

Et OWASP le rend encore pire, mettant l'accent sur l'évasion entrée de l'utilisateur ce qui est une absurdité totale: il ne devrait pas y avoir de tels mots dans le contexte de la protection par injection. Chaque variable est potentiellement dangereuse - peu importe la source! Ou, en d'autres termes - chaque variable doit être correctement formatée pour être mise dans une requête - peu importe la source à nouveau. C'est la destination qui compte. Au moment où un développeur commence à séparer les moutons des chèvres (en pensant si une variable particulière est «sûre» ou non), il fait son premier pas vers le désastre. Sans oublier que même le libellé suggère une évasion en bloc au point d'entrée, ressemblant à la caractéristique des citations très magiques - déjà méprisée, dépréciée et supprimée.

Donc, contrairement à tout ce qui "s'échappe", les déclarations préparées est la mesure qui protège en effet de l'injection SQL (le cas échéant).

Si vous n'êtes toujours pas convaincu, voici une explication étape par étape que j'ai écrite, Guide de l'auto-stoppeur sur la prévention de l'injection SQL, où j'ai expliqué toutes ces questions en détail et même compilé une section entièrement consacrée aux mauvaises pratiques et à leur divulgation.


940



Je recommande d'utiliser AOP (PHP Data Objects) pour exécuter des requêtes SQL paramétrées.

Cela protège non seulement contre l'injection SQL, mais accélère également les requêtes.

Et en utilisant PDO plutôt que mysql_, mysqli_, et pgsql_ fonctions, vous rendez votre application un peu plus abstraite de la base de données, dans le cas rare où vous devez changer de fournisseur de base de données.


768



Utilisation PDO et requêtes préparées.

($conn est un PDO objet)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();

565



Comme vous pouvez le voir, les gens vous suggèrent d'utiliser des déclarations préparées au maximum. Ce n'est pas faux, mais quand votre requête est exécutée juste une fois par processus, il y aurait une légère pénalité de performance.

Je faisais face à ce problème, mais je pense que je l'ai résolu en très façon sophistiquée - la façon dont les pirates utilisent pour éviter d'utiliser des citations. J'ai utilisé ceci en conjonction avec des déclarations préparées émulées. Je l'utilise pour prévenir tout types d'attaques par injection SQL possibles.

Mon approche:

  • Si vous pensez que les entrées sont entières, assurez-vous vraiment entier. Dans un langage de type variable comme PHP, c'est très important. Vous pouvez utiliser par exemple cette solution très simple mais puissante: sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input); 

  • Si vous attendez autre chose d'entier l'hex. Si vous l'expulsez, vous pourrez parfaitement échapper à toute entrée. En C / C ++ il y a une fonction appelée mysql_hex_string(), en PHP, vous pouvez utiliser bin2hex().

    Ne vous inquiétez pas de ce que la chaîne échappée aura une taille 2x de sa longueur d'origine, car même si vous utilisez mysql_real_escape_string, PHP doit allouer la même capacité ((2*input_length)+1), qui est le même.

  • Cette méthode hexadécimale est souvent utilisée lorsque vous transférez des données binaires, mais je ne vois aucune raison de ne pas l'utiliser sur toutes les données pour empêcher les attaques par injection SQL. Notez que vous devez ajouter des données avec 0x ou utilisez la fonction MySQL UNHEX au lieu.

Ainsi, par exemple, la requête:

SELECT password FROM users WHERE name = 'root'

Va devenir:

SELECT password FROM users WHERE name = 0x726f6f74

ou

SELECT password FROM users WHERE name = UNHEX('726f6f74')

Hex est l'évasion parfaite. Pas moyen d'injecter.

Différence entre la fonction UNHEX et le préfixe 0x

Il y a eu des discussions dans les commentaires, alors je tiens à préciser. Ces deux approches sont très similaires, mais elles sont un peu différentes à certains égards:

Le préfixe ** 0x ** ne peut être utilisé que pour des colonnes de données telles que char, varchar, texte, bloc, binaire, etc..
En outre, son utilisation est un peu compliquée si vous êtes sur le point d'insérer une chaîne vide. Vous devrez entièrement le remplacer par ''ou vous obtiendrez une erreur

UNHEX () travaille sur tout colonne; vous n'avez pas à vous soucier de la chaîne vide.


Les méthodes hexadécimales sont souvent utilisées comme des attaques

Notez que cette méthode hexadécimale est souvent utilisée comme une attaque par injection SQL où les entiers sont juste comme des chaînes et s'échappent juste avec mysql_real_escape_string. Ensuite, vous pouvez éviter l'utilisation de citations.

Par exemple, si vous faites juste quelque chose comme ceci:

"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])

une attaque peut vous injecter très facilement. Considérez le code injecté suivant renvoyé à partir de votre script:

SELECT ... OERE id = -1 union tous select table_name à partir de information_schema.tables

et maintenant il suffit d'extraire la structure de la table:

SELECT ... WHERE id = -1 union tout sélectionner nom_colonne à partir de information_schema.column où nom_table = 0x61727469636c65

Et puis sélectionnez simplement les données que vous voulez. N'est-ce pas cool?

Mais si le codeur d'un site injectable l'hexait, aucune injection ne serait possible car la requête ressemblerait à ceci: SELECT ... WHERE id = UNHEX('2d312075...3635')


498



IMPORTANT

La meilleure façon d'empêcher l'injection SQL est d'utiliser Déclarations préparées  au lieu de s'échapper, comme la réponse acceptée démontre.

Il y a des bibliothèques telles que Aura.Sql et EasyDB qui permettent aux développeurs d'utiliser les instructions préparées plus facilement. Pour en savoir plus sur les raisons pour lesquelles les déclarations préparées sont meilleures arrêt de l'injection SQL, faire référence à ce mysql_real_escape_string() contourne et vulnérabilités Unicode SQL Injection récemment corrigées dans WordPress.

Prévention de l'injection - mysql_real_escape_string ()

PHP a une fonction spécialement conçue pour empêcher ces attaques. Tout ce que vous devez faire est d'utiliser la bouchée d'une fonction, mysql_real_escape_string.

mysql_real_escape_string prend une chaîne qui va être utilisée dans une requête MySQL et renvoie la même chaîne avec toutes les tentatives d'injection SQL en toute sécurité échappées. Fondamentalement, il va remplacer ces citations gênantes (') qu'un utilisateur peut entrer avec un substitut MySQL-safe, une citation échappée \'.

REMARQUE: vous devez être connecté à la base de données pour utiliser cette fonction!

// Connexion à MySQL

$name_bad = "' OR 1'"; 

$name_bad = mysql_real_escape_string($name_bad);

$query_bad = "SELECT * FROM customers WHERE username = '$name_bad'";
echo "Escaped Bad Injection: <br />" . $query_bad . "<br />";


$name_evil = "'; DELETE FROM customers WHERE 1 or username = '"; 

$name_evil = mysql_real_escape_string($name_evil);

$query_evil = "SELECT * FROM customers WHERE username = '$name_evil'";
echo "Escaped Evil Injection: <br />" . $query_evil;

Vous pouvez trouver plus de détails dans MySQL - Prévention de l'injection SQL.


452



Avertissement de sécurité: Cette réponse n'est pas conforme aux meilleures pratiques de sécurité. L'échappement est insuffisant pour empêcher l'injection SQL, utilisation déclarations préparées au lieu. Utilisez la stratégie décrite ci-dessous à vos risques et périls. (Aussi, mysql_real_escape_string() a été supprimé en PHP 7.)

Vous pourriez faire quelque chose de fondamental comme ceci:

$safe_variable = mysql_real_escape_string($_POST["user-input"]);
mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

Cela ne résoudra pas tous les problèmes, mais c'est un très bon tremplin. J'ai omis des éléments évidents tels que la vérification de l'existence de la variable, le format (chiffres, lettres, etc.).


410



Quoi que vous finissiez par utiliser, assurez-vous de vérifier que votre entrée n'a pas déjà été altérée par magic_quotes ou d'autres déchets bien intentionnés, et si nécessaire, passez-le à travers stripslashes ou quoi que ce soit pour l'assainir.


345



La requête paramétrée ET la validation d'entrée est la voie à suivre. Il existe de nombreux scénarios sous lesquels l'injection SQL peut se produire, même si mysql_real_escape_string() a été utilisé.

Ces exemples sont vulnérables à l'injection SQL:

$offset = isset($_GET['o']) ? $_GET['o'] : 0;
$offset = mysql_real_escape_string($offset);
RunQuery("SELECT userid, username FROM sql_injection_test LIMIT $offset, 10");

ou

$order = isset($_GET['o']) ? $_GET['o'] : 'userid';
$order = mysql_real_escape_string($order);
RunQuery("SELECT userid, username FROM sql_injection_test ORDER BY `$order`");

Dans les deux cas, vous ne pouvez pas utiliser ' pour protéger l'encapsulation.

La source: L'injection SQL inattendue (lorsque l'échappement n'est pas suffisant)


328



À mon avis, la meilleure façon d'empêcher généralement l'injection SQL dans votre application PHP (ou n'importe quelle application web, d'ailleurs) est de penser à l'architecture de votre application. Si la seule façon de vous protéger contre l'injection SQL est de ne pas oublier d'utiliser une méthode ou une fonction spéciale qui agit correctement à chaque fois que vous parlez à la base de données, vous vous trompez. De cette façon, c'est juste une question de temps jusqu'à ce que vous oubliez de formater correctement votre requête à un moment donné dans votre code.

Adopter le modèle MVC et un cadre comme CakePHP ou CodeIgniter est probablement la bonne voie à suivre: Des tâches courantes telles que la création de requêtes de base de données sécurisées ont été résolues et implémentées de manière centralisée dans de tels cadres. Ils vous aident à organiser votre application Web de manière rationnelle et vous incitent à réfléchir davantage au chargement et à l'enregistrement des objets qu'à la construction sécurisée de requêtes SQL uniques.


280