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

2773

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

$unsafe_variable = $_POST['user_input']; 

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

En effet, l'utilisateur peut saisir 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 éviter que cela se produise?

Andrew G. Johnson
la source

Réponses:

8961

Utilisez des instructions préparées et des requêtes paramétrées. Il s'agit d'instructions SQL envoyées et analysées par le serveur de base de données indépendamment de tout paramètre. De cette façon, il est impossible pour un attaquant d'injecter du SQL malveillant.

Vous avez essentiellement deux options pour y parvenir:

  1. Utilisation de PDO (pour tout pilote de base de données pris en charge):

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute([ 'name' => $name ]);
    
    foreach ($stmt as $row) {
        // Do something with $row
    }
    
  2. Utilisation de 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 deuxième option spécifique au pilote à laquelle vous pouvez vous référer (par exemple, pg_prepare()et pg_execute()pour PostgreSQL). PDO est l'option universelle.


Configuration correcte de la connexion

Notez que lorsque vous utilisez PDOpour accéder à une base de données MySQL, les instructions réelles préparées ne sont pas utilisées 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', 'password');

$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 en Fatal Errorcas de problème. Et cela donne au développeur la possibilité de catchtoute erreur qui est thrown comme PDOExceptions.

Ce qui est obligatoire , cependant, est la première setAttribute()ligne, qui indique à PDO de désactiver les instructions préparées émulées et d'utiliser de véritables instructions préparées. Cela garantit que l'instruction et les valeurs ne sont pas analysées par PHP avant de les envoyer au serveur MySQL (ne donnant à un attaquant potentiel aucune chance d'injecter du SQL malveillant).

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


Explication

L'instruction SQL à laquelle vous passez prepareest analysée et compilée par le serveur de base de données. En spécifiant des paramètres (un ?ou un paramètre nommé comme :namedans l'exemple ci-dessus), vous indiquez au moteur de base de données sur lequel vous souhaitez filtrer. Ensuite, lorsque 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 SQL à envoyer à la base de données. Ainsi, en envoyant le SQL réel séparément des paramètres, vous limitez le risque de vous retrouver avec quelque chose que vous ne vouliez pas.

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 effectuer une certaine optimisation, les paramètres peuvent également se terminer par des nombres, bien sûr). Dans l'exemple ci-dessus, si la $namevariable contient 'Sarah'; DELETE FROM employeesle résultat serait simplement une recherche de la chaîne "'Sarah'; DELETE FROM employees", et vous ne vous retrouverez pas avec une table vide .

Un autre avantage de l'utilisation d'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 permettra de gagner en vitesse.

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

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

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

Les instructions préparées peuvent-elles être utilisées pour des 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 fonctionnalités 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 restreint les valeurs possibles.

// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}
PeeHaa
la source
86
Juste pour ajouter parce que je ne l'ai vu nulle part ailleurs ici, une autre ligne de défense est un pare-feu d'application Web (WAF) qui peut avoir des règles définies pour rechercher les attaques par injection SQL:
jkerak
37
De plus, la documentation officielle de mysql_query ne permet d'exécuter qu'une seule requête, donc toute autre requête en plus; est ignoré. Même si cela est déjà obsolète, il existe de nombreux systèmes sous PHP 5.5.0 et qui peuvent utiliser cette fonction. php.net/manual/en/function.mysql-query.php
Randall Valenciano
12
C'est une mauvaise habitude mais c'est une solution post-problème: non seulement pour l'injection SQL mais pour tout type d'injections (par exemple, il y avait un trou d'injection de modèle de vue dans le framework F3 v2) si vous avez un ancien site Web ou une application qui souffre contre les défauts d'injection, une solution consiste à réaffecter les valeurs de vos variables prédéfinies supperglobal comme $ _POST avec des valeurs d'échappement au bootstrap. Par PDO, il est toujours possible d'échapper (également pour les frameworks d'aujourd'hui): substr ($ pdo-> quote ($ str, \ PDO :: PARAM_STR), 1, -1)
Alix
15
Cette réponse n'a pas d'explication sur ce qu'est une instruction préparée - une chose - c'est un impact sur les performances si vous utilisez beaucoup d'instructions préparées lors de votre demande et parfois cela représente 10 fois les performances. Il serait préférable d'utiliser PDO avec la liaison des paramètres désactivée, mais la préparation des instructions désactivée.
donis
6
L'utilisation de PDO est meilleure, au cas où vous utilisez une requête directe, assurez-vous d'utiliser mysqli :: escape_string
Kassem Itani
1652

Avertissement obsolète: l'exemple de code de cette réponse (comme l'exemple de code de la question) utilise l' MySQLextension PHP , qui est obsolète en PHP 5.5.0 et supprimée entièrement en PHP 7.0.0.

Avertissement de sécurité : cette réponse n'est pas conforme aux meilleures pratiques de sécurité. L'échappement n'est pas suffisant pour empêcher l'injection SQL , utilisez plutôt des instructions préparées . Utilisez la stratégie décrite ci-dessous à vos risques et périls. (En outre, a mysql_real_escape_string()été supprimé en PHP 7.)

Si vous utilisez une version récente de PHP, l' mysql_real_escape_stringoption décrite ci-dessous ne sera plus disponible (bien qu'il s'agisse d' mysqli::escape_stringun équivalent moderne). De nos jours, cette mysql_real_escape_stringoption n'aurait de sens que pour le code hérité d'une ancienne version de PHP.


Vous avez deux options - échapper les caractères spéciaux dans votre unsafe_variable, ou utiliser une requête paramétrée. Les deux vous protégeraient de l'injection SQL. La requête paramétrée est considérée comme la meilleure pratique mais nécessitera de passer à une extension MySQL plus récente en PHP avant de pouvoir l'utiliser.

Nous couvrirons la chaîne à faible impact qui en échappe une 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_stringfonction.

Pour utiliser la requête paramétrée, vous devez utiliser MySQLi plutôt que les fonctions MySQL . 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 voudrez lire là-dessus serait mysqli::prepare.

En outre, comme d'autres l'ont suggéré, vous pouvez trouver utile / plus facile d'intensifier une couche d'abstraction avec quelque chose comme PDO .

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

  • Si vous souhaitez modifier la structure du SQL en fonction de l'entrée 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 type de cas, vous feriez mieux de passer l'entrée de l'utilisateur via une liste blanche pour vous assurer que seules les valeurs «sûres» sont autorisées.
  • Si vous utilisez des entiers à partir de l'entrée utilisateur dans une condition et adoptez l' mysql_real_escape_stringapproche, vous souffrirez du problème décrit par Polynomial dans les commentaires ci-dessous. Ce cas est plus délicat car les entiers ne seraient pas entourés de guillemets, vous pouvez donc y faire face en validant que l'entrée utilisateur ne contient que des chiffres.
  • Il y a probablement d'autres cas que je ne connais pas. Vous trouverez peut - être c'est une ressource utile sur certains des problèmes les plus subtils que vous pouvez rencontrer.
Matt Sheppard
la source
1
utiliser mysql_real_escape_stringest suffisant ou je dois aussi utiliser paramétré?
peiman F.
5
@peimanF. garder une bonne pratique d'utilisation des requêtes paramétrées, même sur un projet local. Avec les requêtes paramétrées, vous êtes assuré qu'il n'y aura pas d'injection SQL. Mais gardez à l'esprit que vous devez aseptiser les données pour éviter la récupération bidon (c'est-à-dire l'injection XSS, comme mettre du code HTML dans un texte) avec htmlentitiespar exemple
Goufalite
2
@peimanF. Bonne pratique pour paramétrer les requêtes et les valeurs de liaison, mais la vraie chaîne d'échappement est bonne pour l'instant
Richard
Je comprends l'inclusion de mysql_real_escape_string()pour être complet, mais je ne suis pas un fan de la liste de l'approche la plus sujette aux erreurs en premier. Le lecteur pourrait simplement saisir rapidement le premier exemple. Heureusement, il est obsolète maintenant :)
Steen Schütt
1
@ SteenSchütt - Toutes les mysql_*fonctions sont obsolètes. Ils ont été remplacés par des mysqli_* fonctions similaires , telles que mysqli_real_escape_string.
Rick James
1073

Chaque réponse ici ne couvre qu'une partie du problème. En fait, il existe quatre parties de requête différentes que nous pouvons ajouter dynamiquement à SQL: -

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

Et les déclarations préparées n'en couvrent que deux.

Mais parfois, nous devons rendre notre requête encore plus dynamique, en ajoutant également 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 la 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 effectuer un classement dynamique:

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

Pour faciliter le processus, j'ai écrit une fonction d'aide à la liste blanche qui fait tout le travail sur une seule ligne:

$orderby = white_list($_GET['orderby'], "name", ["name","price","qty"], "Invalid field name");
$query  = "SELECT * FROM `table` ORDER BY `$orderby`"; // sound and safe

Il existe une autre façon de sécuriser les identifiants - échapper mais je préfère m'en tenir à la liste blanche comme une approche plus robuste et explicite. Pourtant, tant que vous avez un identifiant entre guillemets, vous pouvez échapper le caractère de citation pour le rendre sûr. Par exemple, par défaut pour mysql, vous devez doubler le caractère de citation pour l'échapper . Pour les autres autres règles d'échappement du SGBD, ce serait différent.

Il existe toujours un problème avec les mots clés de syntaxe SQL (tels que AND, DESCet tels), mais la liste blanche semble la seule approche dans ce cas.

Ainsi, une recommandation générale peut être formulée comme

  • Toute variable qui représente un littéral de données SQL (ou, pour le dire simplement - une chaîne SQL ou un nombre) doit être ajoutée via une instruction préparée. Aucune exception.
  • Toute autre partie de requête, telle qu'un mot clé SQL, un nom de table ou de champ, ou un opérateur - doit être filtrée à travers une liste blanche.

Mise à jour

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

Par exemple, il y a (1) (2) encore (3) plusieurs (4) réponses (5) , y compris la deuxième réponse la plus votée suggérant que vous échappiez manuellement les chaînes - une approche obsolète qui s'est avéré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 la revendique même comme la panacée ultime. Bien sûr, ce n'est pas le cas. Cette méthode n'est pas meilleure que le formatage de chaîne normal, mais elle conserve tous ses inconvénients: elle s'applique uniquement aux chaînes et, comme tout autre formatage manuel, c'est essentiellement une mesure facultative et non obligatoire, sujette à des erreurs humaines de toute sorte.

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

Indépendamment de ce que le manuel PHP a dit depuis des lustres, cela *_escape_stringne sécurise en aucun cas les données et n'a jamais été destiné. En plus d'être inutile pour toute partie SQL autre que la chaîne, l'échappement manuel est incorrect, car il est manuel contrairement à automatisé.

Et OWASP aggrave encore la situation, en insistant sur la fuite des entrées utilisateur, ce qui est un non-sens absolu: il ne devrait pas y avoir de tels mots dans le contexte de la protection contre les injections. Chaque variable est potentiellement dangereuse - quelle que soit la source! Ou, en d'autres termes - chaque variable doit être correctement formatée pour être placée dans une requête - quelle que soit la source. 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 / elle fait son premier pas vers la catastrophe. Sans oublier que même le libellé suggère une fuite en masse au point d'entrée, ressemblant à la fonction de citations très magiques - déjà méprisée, déconseillée et supprimée.

Ainsi, contrairement à tout "échappement", les instructions préparées sont la mesure qui protège effectivement contre l'injection SQL (le cas échéant).

votre bon sens
la source
848

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

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

Et en utilisant PDO plutôt que mysql_, mysqli_et les pgsql_fonctions, vous rendez votre application un peu plus abstraite de la base de données, dans les rares cas où vous devez changer de fournisseur de base de données.

Kibbee
la source
619

Utiliser PDOet préparer les requêtes.

( $connest un PDOobjet)

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

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 lorsque votre requête est exécutée une seule fois par processus, il y aura une légère baisse des performances.

J'étais confronté à ce problème, mais je pense que je l'ai résolu de manière très sophistiquée - la façon dont les pirates informatiques utilisent pour éviter d'utiliser des guillemets. J'ai utilisé ceci en conjonction avec des déclarations préparées émulées. Je l'utilise pour empêcher toutes sortes d'attaques par injection SQL possibles.

Mon approche:

  • Si vous vous attendez à ce que l'entrée soit un entier, assurez-vous qu'il est 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 de l' hexagone entier, il . Si vous le sortez, vous échapperez parfaitement à toute entrée. En C / C ++, il existe une fonction appelée mysql_hex_string(), en PHP, vous pouvez utiliser bin2hex().

    Ne vous inquiétez pas du fait 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 la 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 0xou utiliser la fonction MySQL à la UNHEXplace.

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 de s'injecter.

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

Il y a eu une discussion dans les commentaires, donc je veux enfin être clair. Ces deux approches sont très similaires, mais elles sont un peu différentes à certains égards:

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

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


Les méthodes hexagonales sont souvent utilisées comme attaques

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

Par exemple, si vous faites simplement 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é par votre script:

SELECT ... WHERE id = -1 union all sélectionnez table_name dans information_schema.tables

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

SELECT ... WHERE id = -1 union all sélectionnez column_name dans information_schema.column où table_name = 0x61727469636c65

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

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

Zaffy
la source
6
@SumitGupta Oui, vous l'avez fait. MySQL ne concatène pas avec +mais avec CONCAT. Et pour les performances: je ne pense pas que cela affecte les performances car mysql doit analyser les données et peu importe si l'origine est une chaîne ou un hex
Zaffy
11
@YourCommonSense Vous ne comprenez pas le concept ... Si vous voulez avoir une chaîne dans mysql, vous le citez comme ceci 'root'ou vous pouvez l'enserrer 0x726f6f74MAIS si vous voulez un nombre et l'envoyer sous forme de chaîne, vous écrirez probablement '42' et non CHAR (42 ) ... '42' dans l'hex ne serait 0x3432pas0x42
Zaffy
7
@YourCommonSense Je n'ai rien à dire ... juste lol ... si vous voulez toujours essayer hex sur les champs numériques, voir le deuxième commentaire. Je parie avec toi que ça marchera.
Zaffy
2
La réponse indique clairement que cela ne fonctionnera pas de cette façon avec des valeurs entières, la raison étant que bin2hex convertit la valeur transmise en chaîne (et donc bin2hex (0) est 0x30, et non 0x03) - c'est probablement la partie qui vous confond . Si vous suivez cela, cela fonctionne parfaitement (au moins sur mon site, testé avec 4 versions différentes de mysql sur les machines Debian, 5.1.x à 5.6.x). Après tout, l'hexadécimal n'est que le moyen de représentation, pas la valeur;)
griffin
5
@YourCommonSense vous ne comprenez toujours pas? Vous ne pouvez pas utiliser 0x et concat parce que si la chaîne est vide, vous finirez avec une erreur. Si vous voulez une alternative simple à votre requête, essayez celle-ciSELECT title FROM article WHERE id = UNHEX(' . bin2hex($_GET["id"]) . ')
Zaffy
499

Avertissement obsolète: l'exemple de code de cette réponse (comme l'exemple de code de la question) utilise l' MySQLextension PHP , qui est obsolète en PHP 5.5.0 et supprimée entièrement en PHP 7.0.0.

Avertissement de sécurité : cette réponse n'est pas conforme aux meilleures pratiques de sécurité. L'échappement n'est pas suffisant pour empêcher l'injection SQL , utilisez plutôt des instructions préparées . Utilisez la stratégie décrite ci-dessous à vos risques et périls. (En outre, a mysql_real_escape_string()été supprimé en PHP 7.)

IMPORTANT

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

Il existe des bibliothèques telles que Aura.Sql et EasyDB qui permettent aux développeurs d'utiliser plus facilement les instructions préparées. Pour en savoir plus sur les raisons pour lesquelles les instructions préparées sont plus efficaces pour arrêter l'injection SQL , reportez-vous à ce mysql_real_escape_string()contournement et aux vulnérabilités d'injection SQL Unicode récemment corrigées dans WordPress .

Prévention des injections - mysql_real_escape_string ()

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

mysql_real_escape_stringprend une chaîne qui va être utilisée dans une requête MySQL et retourne la même chaîne avec toutes les tentatives d'injection SQL échappées en toute sécurité. Fondamentalement, il remplacera les guillemets gênants (') qu'un utilisateur pourrait entrer avec un substitut sûr MySQL, un guillemet échappé \'.

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

// Connectez-vous à 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 - SQL Injection Prevention .

rahularyansharma
la source
30
C'est le meilleur que vous puissiez faire avec l'extension mysql héritée. Pour le nouveau code, il est conseillé de passer à mysqli ou PDO.
Álvaro González
7
Je ne suis pas d'accord avec cette «fonction spécialement conçue pour empêcher ces attaques». Je pense que ce mysql_real_escape_stringbut est de permettre de construire une requête SQL correcte pour chaque chaîne de données d'entrée. La prévention sql-injection est l'effet secondaire de cette fonction.
sectus
4
vous n'utilisez pas de fonctions pour écrire des chaînes de données d'entrée correctes. Vous écrivez juste ceux qui n'ont pas besoin d'être échappés ou qui ont déjà été échappés. mysql_real_escape_string () peut avoir été conçu dans le but que vous mentionnez, mais sa seule valeur est d'empêcher l'injection.
Nazca
17
ATTENTION! mysql_real_escape_string() n'est pas infaillible .
eggyal
9
mysql_real_escape_stringest désormais obsolète, ce n'est donc plus une option viable. Il sera supprimé à l'avenir de PHP. Il est préférable de passer à ce que les gens de PHP ou de MySQL recommandent.
juillet 2015 à 6h41
462

Vous pouvez faire quelque chose de basique comme ceci:

$safe_variable = mysqli_real_escape_string($_POST["user-input"], $dbConnection);
mysqli_query($dbConnection, "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, du format de la variable (chiffres, lettres, etc.).

Kiran Maniya
la source
28
J'ai essayé votre exemple et cela fonctionne très bien pour moi.Pouvez-vous effacer "cela ne résoudra pas tous les problèmes"
Chinook
14
Si vous ne citez pas la chaîne, elle est toujours injectable. Prenez $q = "SELECT col FROM tbl WHERE x = $safe_var";par exemple. Réglage $safe_varde 1 UNION SELECT password FROM userstravaux dans ce cas en raison du manque de citations. Il est également possible d'injecter des chaînes dans la requête à l'aide de CONCATet CHR.
Polynôme
4
@Polynomial Tout à fait raison, mais je ne verrais cela que comme une mauvaise utilisation. Tant que vous l'utilisez correctement, cela fonctionnera certainement.
glglgl
22
ATTENTION! mysql_real_escape_string() n'est pas infaillible .
eggyal
8
mysql_real_escape_stringest désormais obsolète, ce n'est donc plus une option viable. Il sera supprimé à l'avenir de PHP. Il est préférable de passer à ce que les gens de PHP ou de MySQL recommandent.
juillet 2015
380

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_quotesou d'autres déchets bien intentionnés, et si nécessaire, exécutez-la stripslashesou autre chose pour la désinfecter.

Rob
la source
11
En effet; courir avec magic_quotes activé encourage simplement les mauvaises pratiques. Cependant, parfois, vous ne pouvez pas toujours contrôler l'environnement à ce niveau - soit vous n'avez pas accès à la gestion du serveur, soit votre application doit coexister avec des applications qui (frémissent) dépendent d'une telle configuration. Pour ces raisons, il est bon d'écrire des applications portables - même si, évidemment, l'effort est gaspillé si vous contrôlez l'environnement de déploiement, par exemple parce que c'est une application interne ou que vous n'allez l'utiliser que dans votre environnement spécifique.
Rob
24
Depuis PHP 5.4, l'abomination connue sous le nom de «guillemets magiques» a été tuée . Et bon débarras aux mauvais déchets.
BryanH
363

Avertissement obsolète: l'exemple de code de cette réponse (comme l'exemple de code de la question) utilise l' MySQLextension PHP , qui est obsolète en PHP 5.5.0 et supprimée entièrement en PHP 7.0.0.

Avertissement de sécurité : cette réponse n'est pas conforme aux meilleures pratiques de sécurité. L'échappement n'est pas suffisant pour empêcher l'injection SQL , utilisez plutôt des instructions préparées . Utilisez la stratégie décrite ci-dessous à vos risques et périls. (En outre, a mysql_real_escape_string()été supprimé en PHP 7.)

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

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.

Source : l' injection SQL inattendue (lorsque l'échappement ne suffit pas)

Cédric
la source
2
Vous pouvez empêcher l'injection SQL si vous adoptez une technique de validation d'entrée dans laquelle l'entrée utilisateur est authentifiée par rapport à un ensemble de règles définies pour la longueur, le type et la syntaxe, ainsi que par rapport aux règles métier.
Josip Ivic
312

À 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 se protéger contre l'injection SQL est de se souvenir d'utiliser une méthode ou une fonction spéciale qui fait la bonne chose chaque fois que vous parlez à la base de données, vous vous trompez. De cette façon, ce n'est qu'une question de temps jusqu'à ce que vous oubliez de formater correctement votre requête à un moment donné de votre code.

L'adoption du modèle MVC et d'un cadre comme CakePHP ou CodeIgniter est probablement la bonne solution: 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 mises en œuvre de manière centralisée dans de tels cadres. Ils vous aident à organiser votre application Web de manière sensée et vous font penser davantage au chargement et à l'enregistrement d'objets qu'à la construction sécurisée de requêtes SQL uniques.

Johannes Fahrenkrug
la source
5
Je pense que votre premier paragraphe est important. La compréhension est la clé. De plus, tout le monde ne travaille pas pour une entreprise. Pour un grand nombre de personnes, les cadres vont à l'encontre de l'idée de comprendre . Se familiariser avec les principes fondamentaux peut ne pas être apprécié tout en travaillant dans un délai, mais les bricoleurs aiment se salir les mains. Les développeurs de frameworks ne sont pas si privilégiés que tout le monde doit s'incliner et supposer qu'ils ne font jamais d'erreurs. Le pouvoir de prendre des décisions est toujours important. Qui peut dire que mon cadre ne remplacera pas un autre régime à l'avenir?
Anthony Rutledge
@AnthonyRutledge Vous avez absolument raison. Il est très important de comprendre ce qui se passe et pourquoi. Cependant, la chance qu'un framework éprouvé et activement utilisé et développé ait rencontré et résolu de nombreux problèmes et corrigé de nombreux trous de sécurité est déjà assez élevée. C'est une bonne idée de regarder la source pour avoir une idée de la qualité du code. S'il s'agit d'un gâchis non testé, il n'est probablement pas sécurisé.
Johannes Fahrenkrug
3
Ici. Ici. Bons points. Cependant, seriez-vous d'accord que de nombreuses personnes peuvent étudier et apprendre à adopter un système MVC, mais tout le monde ne peut pas le reproduire à la main (contrôleurs et serveur). On peut aller trop loin avec ce point. Dois-je comprendre mon micro-ondes avant de chauffer mes biscuits aux noix de pécan et au beurre d'arachide que ma copine m'a fait? ;-)
Anthony Rutledge
3
@AnthonyRutledge, je suis d'accord! Je pense que le cas d'utilisation fait également la différence: est-ce que je crée une galerie de photos pour ma page d'accueil personnelle ou est-ce que je crée une application Web de banque en ligne? Dans ce dernier cas, il est très important de comprendre les détails de la sécurité et comment un cadre que j'utilise y répond.
Johannes Fahrenkrug
3
Ah, l'exception de sécurité au corollaire du bricolage. Vous voyez, j'ai tendance à tout risquer et à faire faillite. :-) Je plaisante. Avec assez de temps, les gens peuvent apprendre à faire une application très sécurisée. Trop de gens sont pressés. Ils lèvent la main et supposent que les cadres sont plus sûrs . Après tout, ils n'ont pas assez de temps pour tester et comprendre les choses. De plus, la sécurité est un domaine qui nécessite une étude dédiée. Ce n'est pas quelque chose que les simples programmeurs connaissent en profondeur grâce à la compréhension des algorithmes et des modèles de conception.
Anthony Rutledge
298

Je privilégie les procédures stockées ( MySQL prend en charge les procédures stockées depuis 5.0 ) d'un point de vue sécurité - les avantages sont -

  1. La plupart des bases de données (y compris MySQL ) permettent de restreindre l'accès des utilisateurs à l'exécution de procédures stockées. Le contrôle d'accès de sécurité à grain fin est utile pour empêcher l'escalade des attaques de privilèges. Cela empêche les applications compromises de pouvoir exécuter SQL directement sur la base de données.
  2. Ils font abstraction de la requête SQL brute de l'application afin que moins d'informations sur la structure de la base de données soient disponibles pour l'application. Cela rend plus difficile la compréhension de la structure sous-jacente de la base de données et la conception d'attaques appropriées.
  3. Ils n'acceptent que les paramètres, les avantages des requêtes paramétrées sont donc là. Bien sûr - IMO, vous devez toujours nettoyer votre entrée - surtout si vous utilisez du SQL dynamique dans la procédure stockée.

Les inconvénients sont -

  1. Ils (procédures stockées) sont difficiles à maintenir et ont tendance à se multiplier très rapidement. Cela rend leur gestion problématique.
  2. Ils ne sont pas très adaptés aux requêtes dynamiques - s'ils sont conçus pour accepter du code dynamique comme paramètres, de nombreux avantages sont annulés.
Nikhil
la source
297

Il existe de nombreuses façons d'empêcher les injections SQL et autres hacks SQL. Vous pouvez facilement le trouver sur Internet (Recherche Google). Bien sûr, PDO est l'une des bonnes solutions. Mais je voudrais vous suggérer une bonne prévention des liens contre l'injection SQL.

Qu'est-ce que l'injection SQL et comment empêcher

Manuel PHP pour l'injection SQL

Explication de Microsoft sur l'injection et la prévention SQL en PHP

Et quelques autres comme Empêcher l'injection SQL avec MySQL et PHP .

Maintenant, pourquoi avez-vous besoin d'empêcher votre requête d'injection SQL?

Je voudrais vous faire savoir: pourquoi essayons-nous d'empêcher l'injection SQL avec un court exemple ci-dessous:

Requête pour la correspondance d'authentification de connexion:

$query="select * from users where email='".$_POST['email']."' and password='".$_POST['password']."' ";

Maintenant, si quelqu'un (un hacker) met

$_POST['email']= admin@emali.com' OR '1=1

et mot de passe quoi que ce soit ....

La requête ne sera analysée dans le système que jusqu'à:

$query="select * from users where email='[email protected]' OR '1=1';

L'autre partie sera jetée. Alors, que va-t-il se passer? Un utilisateur non autorisé (hacker) pourra se connecter en tant qu'administrateur sans avoir son mot de passe. Maintenant, il / elle peut faire tout ce que l'administrateur / email peut faire. Voir, c'est très dangereux si l'injection SQL n'est pas empêchée.

Manish Shrivastava
la source
267

Je pense que si quelqu'un veut utiliser PHP et MySQL ou un autre serveur dataBase:

  1. Pensez à apprendre PDO (PHP Data Objects) - c'est une couche d'accès à la base de données fournissant une méthode uniforme d'accès à plusieurs bases de données.
  2. Pensez à apprendre MySQLi
  3. Utilisez des fonctions PHP natives comme: strip_tags , mysql_real_escape_string ou si variable numérique, juste (int)$foo. En savoir plus sur le type de variables en PHP ici . Si vous utilisez des bibliothèques telles que PDO ou MySQLi, utilisez toujours PDO :: quote () et mysqli_real_escape_string () .

Exemples de bibliothèques:

---- AOP

----- Aucun espace réservé - mûr pour l'injection SQL! C'est mauvais

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values ($name, $addr, $city)");

----- Espaces réservés sans nom

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values (?, ?, ?);

----- Espaces réservés nommés

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) value (:name, :addr, :city)");

--- MySQLi

$request = $mysqliConnection->prepare('
       SELECT * FROM trainers
       WHERE name = ?
       AND email = ?
       AND last_login > ?');

    $query->bind_param('first_param', 'second_param', $mail, time() - 3600);
    $query->execute();

PS :

AOP remporte cette bataille avec facilité. Avec la prise en charge de douze pilotes de base de données différents et de paramètres nommés, nous pouvons ignorer la petite perte de performances et nous habituer à son API. Du point de vue de la sécurité, les deux sont sûrs tant que le développeur les utilise comme ils sont censés être utilisés

Mais alors que PDO et MySQLi sont assez rapides, MySQLi fonctionne de manière insignifiante plus rapidement dans les benchmarks - ~ 2,5% pour les instructions non préparées et ~ 6,5% pour les instructions préparées.

Et veuillez tester chaque requête dans votre base de données - c'est une meilleure façon d'empêcher l'injection.

RDK
la source
que mysqli est incorrect. Le premier paramètre exprime les types de données.
mickmackusa
257

Si possible, transtypez les types de vos paramètres. Mais cela ne fonctionne que sur des types simples comme int, bool et float.

$unsafe_variable = $_POST['user_id'];

$safe_variable = (int)$unsafe_variable ;

mysqli_query($conn, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
devOp
la source
3
C'est l'un des rares cas où j'utiliserais une "valeur d'échappement" au lieu d'une instruction préparée. Et la conversion de type entier est extrêmement efficace.
HoldOffHunger
233

Si vous souhaitez profiter des moteurs de cache, comme Redis ou Memcached , peut-être que DALMP pourrait être un choix. Il utilise MySQLi pur . Vérifiez ceci: DALMP Database Abstraction Layer for MySQL en utilisant PHP.

En outre, vous pouvez «préparer» vos arguments avant de préparer votre requête afin de pouvoir créer des requêtes dynamiques et, à la fin, avoir une requête d'instructions entièrement préparée. Couche d'abstraction de base de données DALMP pour MySQL utilisant PHP.

Peter Mortensen
la source
224

Pour ceux qui ne savent pas comment utiliser PDO (provenant des mysql_fonctions), j'ai fait un wrapper PDO très, très simple qui est un seul fichier. Il existe pour montrer à quel point il est facile de faire toutes les choses courantes que les applications doivent être faites. Fonctionne avec PostgreSQL, MySQL et SQLite.

Fondamentalement, le lire pendant que vous lisez le manuel pour voir comment mettre les fonctions AOP à l' utilisation dans la vie réelle pour le rendre simple pour stocker et récupérer des valeurs dans le format que vous voulez.

Je veux une seule colonne

$count = DB::column('SELECT COUNT(*) FROM `user`);

Je veux un tableau (clé => valeur) (c'est-à-dire pour faire une boîte de sélection)

$pairs = DB::pairs('SELECT `id`, `username` FROM `user`);

Je veux un résultat sur une seule ligne

$user = DB::row('SELECT * FROM `user` WHERE `id` = ?', array($user_id));

Je veux un tableau de résultats

$banned_users = DB::fetch('SELECT * FROM `user` WHERE `banned` = ?', array(TRUE));
Xeoncross
la source
222

En utilisant cette fonction PHP, mysql_escape_string()vous pouvez obtenir une bonne prévention rapidement.

Par exemple:

SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."'

mysql_escape_string - Échappe une chaîne pour une utilisation dans une mysql_query

Pour plus de prévention, vous pouvez ajouter à la fin ...

wHERE 1=1   or  LIMIT 1

Enfin, vous obtenez:

SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."' LIMIT 1
Nicolas Finelli
la source
220

Quelques conseils pour échapper les caractères spéciaux dans les instructions SQL.

N'utilisez pas MySQL . Cette extension est obsolète. Utilisez plutôt MySQLi ou PDO .

MySQLi

Pour échapper manuellement des caractères spéciaux dans une chaîne, vous pouvez utiliser la fonction mysqli_real_escape_string . La fonction ne fonctionnera pas correctement sauf si le jeu de caractères correct est défini avec mysqli_set_charset .

Exemple:

$mysqli = new mysqli('host', 'user', 'password', 'database');
$mysqli->set_charset('charset');

$string = $mysqli->real_escape_string($string);
$mysqli->query("INSERT INTO table (column) VALUES ('$string')");

Pour l'échappement automatique de valeurs avec des instructions préparées, utilisez mysqli_prepare et mysqli_stmt_bind_param où des types pour les variables de liaison correspondantes doivent être fournis pour une conversion appropriée:

Exemple:

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

$stmt->bind_param("is", $integer, $string);

$stmt->execute();

Peu importe si vous utilisez des instructions préparées ou mysqli_real_escape_string, vous devez toujours connaître le type de données d'entrée avec lesquelles vous travaillez.

Donc, si vous utilisez une instruction préparée, vous devez spécifier les types de variables pour la mysqli_stmt_bind_paramfonction.

Et l'utilisation de mysqli_real_escape_stringest pour, comme son nom l'indique, échapper des caractères spéciaux dans une chaîne, donc cela ne rendra pas les entiers sûrs. Le but de cette fonction est d'empêcher la rupture des chaînes dans les instructions SQL et les dommages à la base de données qu'elle pourrait provoquer. mysqli_real_escape_stringest une fonction utile lorsqu'elle est utilisée correctement, en particulier lorsqu'elle est combinée avec sprintf.

Exemple:

$string = "x' OR name LIKE '%John%";
$integer = '5 OR id != 0';

$query = sprintf( "SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 5

$integer = '99999999999999999999';
$query = sprintf("SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 2147483647
Danijel
la source
3
La question est très générique. Quelques bonnes réponses ci-dessus, mais la plupart suggèrent des déclarations préparées. MySQLi async ne prend pas en charge les instructions préparées, le sprintf semble donc être une excellente option pour cette situation.
Dustin Graham
183

L'alternative simple à ce problème pourrait être résolue en accordant les autorisations appropriées dans la base de données elle-même. Par exemple: si vous utilisez une base de données MySQL, entrez dans la base de données via le terminal ou l'interface utilisateur fournie et suivez simplement cette commande:

 GRANT SELECT, INSERT, DELETE ON database TO username@'localhost' IDENTIFIED BY 'password';

Cela limitera l'utilisateur à se limiter uniquement aux requêtes spécifiées. Supprimez l'autorisation de suppression afin que les données ne soient jamais supprimées de la requête renvoyée depuis la page PHP. La deuxième chose à faire est de vider les privilèges afin que MySQL actualise les autorisations et les mises à jour.

FLUSH PRIVILEGES; 

plus d'informations sur flush .

Pour voir les privilèges actuels de l'utilisateur, lancez la requête suivante.

select * from mysql.user where User='username';

En savoir plus sur GRANT .

Apurv Nerlekar
la source
25
Cette réponse est essentiellement fausse , car elle n'aide pas à empêcher une prévention des injections, mais simplement à en atténuer les conséquences. En vain.
Votre bon sens
1
Bien sûr, cela ne fournit pas de solution, mais c'est ce que vous pouvez faire à l'avance pour éviter les choses.
Apurv Nerlekar
1
@Apurv Si mon objectif est de lire des informations privées dans votre base de données, ne pas avoir l'autorisation SUPPRIMER ne signifie rien.
Alex Holsgrove
1
@AlexHolsgrove: Allez-y doucement, je proposais simplement de bonnes pratiques pour atténuer les conséquences.
Apurv Nerlekar
2
@Apurv Vous ne voulez pas "adoucir les conséquences", vous voulez tout faire pour vous en protéger. Pour être juste cependant, il est important de définir le bon accès utilisateur, mais pas vraiment ce que l'OP demande.
Alex Holsgrove
177

En ce qui concerne de nombreuses réponses utiles, j'espère ajouter de la valeur à ce fil.

L'injection SQL est une attaque qui peut être effectuée via des entrées utilisateur (entrées remplies par un utilisateur, puis utilisées dans des requêtes). Les modèles d'injection SQL sont une syntaxe de requête correcte alors que nous pouvons l'appeler: mauvaises requêtes pour de mauvaises raisons, et nous supposons qu'une mauvaise personne peut essayer d'obtenir des informations secrètes (en contournant le contrôle d'accès) qui affectent les trois principes de sécurité (confidentialité , intégrité et disponibilité).

Maintenant, notre objectif est de prévenir les menaces de sécurité telles que les attaques par injection SQL, la question demandant (comment empêcher une attaque par injection SQL en utilisant PHP), être plus réaliste, le filtrage ou la suppression des données d'entrée est le cas lors de l'utilisation de données d'entrée utilisateur à l'intérieur une telle requête, en utilisant PHP ou tout autre langage de programmation n'est pas le cas, ou comme recommandé par plus de gens pour utiliser une technologie moderne comme une instruction préparée ou tout autre outil prenant actuellement en charge la prévention des injections SQL, considérez-vous que ces outils ne sont plus disponibles? Comment sécurisez-vous votre application?

Mon approche contre l'injection SQL est la suivante: effacer les données entrées par l'utilisateur avant de les envoyer à la base de données (avant de les utiliser dans une requête).

Filtrage des données pour (conversion de données non sécurisées en données sécurisées)

Considérez que PDO et MySQLi ne sont pas disponibles. Comment sécuriser votre application? Me forcez-vous à les utiliser? Qu'en est-il des autres langages autres que PHP? Je préfère fournir des idées générales car elles peuvent être utilisées pour une frontière plus large, pas seulement pour une langue spécifique.

  1. Utilisateur SQL (limitation des privilèges utilisateur): les opérations SQL les plus courantes sont (SELECT, UPDATE, INSERT), alors, pourquoi accorder le privilège UPDATE à un utilisateur qui n'en a pas besoin? Par exemple, les pages de connexion et de recherche utilisent uniquement SELECT, alors pourquoi utiliser les utilisateurs de base de données dans ces pages avec des privilèges élevés?

RÈGLE: ne créez pas un utilisateur de base de données pour tous les privilèges. Pour toutes les opérations SQL, vous pouvez créer votre schéma comme (deluser, selectuser, updateuser) en tant que noms d'utilisateur pour une utilisation facile.

Voir principe du moindre privilège .

  1. Filtrage des données: avant de créer une entrée utilisateur de requête, elle doit être validée et filtrée. Pour les programmeurs, il est important de définir certaines propriétés pour chaque variable entrée par l'utilisateur: type de données, modèle de données et longueur des données . Un champ qui est un nombre entre (x et y) doit être exactement validé en utilisant la règle exacte, et pour un champ qui est une chaîne (texte): le modèle est le cas, par exemple, un nom d'utilisateur ne doit contenir que quelques caractères, disons dites [a-zA-Z0-9_-.]. La longueur varie entre (x et n) où x et n (entiers, x <= n). Règle: la création de filtres exacts et de règles de validation sont les meilleures pratiques pour moi.

  2. Utilisez d'autres outils: ici, je conviendrai également avec vous qu'une instruction préparée (requête paramétrée) et des procédures stockées. Les inconvénients ici sont que ces méthodes nécessitent des compétences avancées qui n'existent pas pour la plupart des utilisateurs. L'idée de base ici est de faire la distinction entre la requête SQL et les données utilisées à l'intérieur. Les deux approches peuvent être utilisées même avec des données non sécurisées, car les données saisies par l'utilisateur ici n'ajoutent rien à la requête d'origine, comme (any ou x = x).

Pour plus d'informations, veuillez lire le cheat sheet OWASP SQL Injection Prevention .

Maintenant, si vous êtes un utilisateur avancé, commencez à utiliser cette défense comme vous le souhaitez, mais, pour les débutants, s'ils ne peuvent pas rapidement mettre en œuvre une procédure stockée et préparer l'instruction, il est préférable de filtrer autant que possible les données d'entrée.

Enfin, considérons qu'un utilisateur envoie ce texte ci-dessous au lieu de saisir son nom d'utilisateur:

[1] UNION SELECT IF(SUBSTRING(Password,1,1)='2',BENCHMARK(100000,SHA1(1)),0) User,Password FROM mysql.user WHERE User = 'root'

Cette entrée peut être vérifiée tôt sans instruction préparée ni procédures stockées, mais pour être sûr, leur utilisation commence après le filtrage et la validation des données utilisateur.

Le dernier point est la détection d'un comportement inattendu qui nécessite plus d'efforts et de complexité; ce n'est pas recommandé pour les applications Web normales.

Un comportement inattendu dans l'entrée utilisateur ci-dessus est SELECT, UNION, IF, SUBSTRING, BENCHMARK, SHA et root. Une fois ces mots détectés, vous pouvez éviter la saisie.

MISE À JOUR 1:

Un utilisateur a commenté que ce post est inutile, OK! Voici ce que OWASP.ORG a fourni :

Défenses principales:

Option # 1: Utilisation d'instructions préparées (Requêtes paramétrées)
Option # 2: Utilisation de procédures stockées
Option # 3: Échapper à toutes les entrées fournies par l'utilisateur

Défenses supplémentaires:

Appliquer également: Moins de privilèges
Effectuer également: Validation des entrées de la liste blanche

Comme vous le savez peut-être, la revendication d'un article doit être appuyée par un argument valide, au moins par une référence! Sinon, c'est considéré comme une attaque et une mauvaise réclamation!

Mise à jour 2:

Depuis le manuel PHP, PHP: Instructions préparées - Manuel :

Échappement et injection SQL

Les variables liées seront échappées automatiquement par le serveur. Le serveur insère leurs valeurs d'échappement aux endroits appropriés dans le modèle d'instruction avant l'exécution. Un indice doit être fourni au serveur pour le type de variable liée, afin de créer une conversion appropriée. Voir la fonction mysqli_stmt_bind_param () pour plus d'informations.

L'échappement automatique de valeurs au sein du serveur est parfois considéré comme une fonction de sécurité pour empêcher l'injection SQL. Le même degré de sécurité peut être atteint avec des instructions non préparées si les valeurs d'entrée sont correctement échappées.

Mise à jour 3:

J'ai créé des cas de test pour savoir comment PDO et MySQLi envoient la requête au serveur MySQL lors de l'utilisation d'une instruction préparée:

AOP:

$user = "''1''"; // Malicious keyword
$sql = 'SELECT * FROM awa_user WHERE userame =:username';
$sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$sth->execute(array(':username' => $user));

Journal des requêtes:

    189 Query SELECT * FROM awa_user WHERE userame ='\'\'1\'\''
    189 Quit

MySQLi:

$stmt = $mysqli->prepare("SELECT * FROM awa_user WHERE username =?")) {
$stmt->bind_param("s", $user);
$user = "''1''";
$stmt->execute();

Journal des requêtes:

    188 Prepare   SELECT * FROM awa_user WHERE username =?
    188 Execute   SELECT * FROM awa_user WHERE username ='\'\'1\'\''
    188 Quit

Il est clair qu'une instruction préparée échappe également aux données, rien d'autre.

Comme mentionné également dans la déclaration ci-dessus,

L'échappement automatique de valeurs au sein du serveur est parfois considéré comme une fonction de sécurité pour empêcher l'injection SQL. Le même degré de sécurité peut être atteint avec des instructions non préparées, si les valeurs d'entrée sont correctement échappées

Par conséquent, cela prouve que la validation des données telle que intval()c'est une bonne idée pour les valeurs entières avant d'envoyer une requête. De plus, empêcher les données utilisateur malveillantes avant d'envoyer la requête est une approche correcte et valide .

Veuillez consulter cette question pour plus de détails: PDO envoie une requête brute à MySQL tandis que Mysqli envoie une requête préparée, les deux produisent le même résultat

Références:

  1. Aide-mémoire pour l'injection SQL
  2. Injection SQL
  3. Sécurité de l'information
  4. Principes de sécurité
  5. La validation des données
utilisateur1646111
la source
175

Avertissement de sécurité : cette réponse n'est pas conforme aux meilleures pratiques de sécurité. L'échappement n'est pas suffisant pour empêcher l'injection SQL , utilisez plutôt des instructions préparées . Utilisez la stratégie décrite ci-dessous à vos risques et périls. (En outre, a mysql_real_escape_string()été supprimé en PHP 7.)

Avertissement obsolète: l'extension mysql est obsolète pour le moment. nous vous recommandons d'utiliser l' extension PDO

J'utilise trois façons différentes pour empêcher mon application Web d'être vulnérable à l'injection SQL.

  1. L' utilisation de mysql_real_escape_string(), qui est une fonction prédéfinie dans PHP , et ce module de code anti - slash aux caractères suivants: \x00, \n, \r, \, ', "et \x1a. Passez les valeurs d'entrée en tant que paramètres pour minimiser les risques d'injection SQL.
  2. Le moyen le plus avancé consiste à utiliser des PDO.

J'espère que cela t'aidera.

Considérez la requête suivante:

$iId = mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string () ne protège pas ici. Si vous utilisez des guillemets simples ('') autour de vos variables dans votre requête, c'est ce qui vous protège contre cela. Voici une solution ci-dessous pour cela:

$iId = (int) mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";

Cette question a de bonnes réponses à ce sujet.

Je suggère que l'utilisation de PDO est la meilleure option.

Éditer:

mysql_real_escape_string()est obsolète depuis PHP 5.5.0. Utilisez mysqli ou PDO.

Une alternative à mysql_real_escape_string () est

string mysqli_real_escape_string ( mysqli $link , string $escapestr )

Exemple:

$iId = $mysqli->real_escape_string("1 OR 1=1");
$mysqli->query("SELECT * FROM table WHERE id = $iId");
Soumalya Banerjee
la source
169

Un moyen simple serait d'utiliser un framework PHP comme CodeIgniter ou Laravel qui ont des fonctionnalités intégrées comme le filtrage et l'enregistrement actif afin que vous n'ayez pas à vous soucier de ces nuances.

Deepak Thomas
la source
7
Je pense que l'objectif de la question est de faire cela sans utiliser un tel cadre.
Sanke
147

Avertissement: l'approche décrite dans cette réponse ne s'applique qu'à des scénarios très spécifiques et n'est pas sécurisée car les attaques par injection SQL ne reposent pas uniquement sur la possibilité d'injecter X=Y.

Si les attaquants tentent de pirater le formulaire via la $_GETvariable PHP ou avec la chaîne de requête de l'URL, vous seriez en mesure de les attraper s'ils ne sont pas sécurisés.

RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+)
RewriteRule ^(.*) ^/track.php

Parce que 1=1, 2=2, 1=2, 2=1, 1+1=2, etc ... sont les questions communes à une base de données SQL d'un attaquant. Peut-être aussi qu'il est utilisé par de nombreuses applications de piratage.

Mais vous devez être prudent, que vous ne devez pas réécrire une requête sûre depuis votre site. Le code ci-dessus vous donne un conseil, pour réécrire ou rediriger (cela dépend de vous) cette chaîne de requête dynamique spécifique au piratage dans une page qui stockera l' adresse IP de l'attaquant , ou MÊME LEURS COOKIES, l'historique, le navigateur ou tout autre élément sensible informations, afin que vous puissiez les traiter plus tard en interdisant leur compte ou en contactant les autorités.

5ervant
la source
Qu'est-ce qui se passe 1-1=0? :)
Rápli András
@ RápliAndrás Une sorte de ([0-9\-]+)=([0-9]+).
5ervant
127

Il y a tellement de réponses pour PHP et MySQL , mais voici du code pour PHP et Oracle pour empêcher l'injection SQL ainsi que l'utilisation régulière des pilotes oci8:

$conn = oci_connect($username, $password, $connection_string);
$stmt = oci_parse($conn, 'UPDATE table SET field = :xx WHERE ID = 123');
oci_bind_by_name($stmt, ':xx', $fieldval);
oci_execute($stmt);
Chintan Gor
la source
Veuillez expliquer les paramètres oci_bind_by_name.
Jahanzeb Awan
127

Une bonne idée est d'utiliser un mappeur relationnel objet comme Idiorm :

$user = ORM::for_table('user')
->where_equal('username', 'j4mie')
->find_one();

$user->first_name = 'Jamie';
$user->save();

$tweets = ORM::for_table('tweet')
    ->select('tweet.*')
    ->join('user', array(
        'user.id', '=', 'tweet.user_id'
    ))
    ->where_equal('user.username', 'j4mie')
    ->find_many();

foreach ($tweets as $tweet) {
    echo $tweet->text;
}

Cela vous évite non seulement les injections SQL, mais aussi les erreurs de syntaxe! Il prend également en charge des collections de modèles avec un chaînage de méthodes pour filtrer ou appliquer des actions à plusieurs résultats à la fois et à plusieurs connexions.

Thomas Ahle
la source
124

Avertissement obsolète: l'exemple de code de cette réponse (comme l'exemple de code de la question) utilise l' MySQLextension PHP , qui est obsolète en PHP 5.5.0 et supprimée entièrement en PHP 7.0.0.

Avertissement de sécurité : cette réponse n'est pas conforme aux meilleures pratiques de sécurité. L'échappement n'est pas suffisant pour empêcher l'injection SQL , utilisez plutôt des instructions préparées . Utilisez la stratégie décrite ci-dessous à vos risques et périls. (En outre, a mysql_real_escape_string()été supprimé en PHP 7.)

L'utilisation de PDO et MYSQLi est une bonne pratique pour empêcher les injections SQL, mais si vous voulez vraiment travailler avec les fonctions et requêtes MySQL, il serait préférable d'utiliser

mysql_real_escape_string

$unsafe_variable = mysql_real_escape_string($_POST['user_input']);

Il y a plus de capacités pour empêcher cela: comme identifier - si l'entrée est une chaîne, un nombre, un caractère ou un tableau, il y a tellement de fonctions intégrées pour détecter cela. En outre, il serait préférable d'utiliser ces fonctions pour vérifier les données d'entrée.

is_string

$unsafe_variable = (is_string($_POST['user_input']) ? $_POST['user_input'] : '');

is_numeric

$unsafe_variable = (is_numeric($_POST['user_input']) ? $_POST['user_input'] : '');

Et il est tellement préférable d'utiliser ces fonctions pour vérifier les données d'entrée avec mysql_real_escape_string.

Rakesh Sharma
la source
10
En outre, il est absolument inutile de vérifier les membres du tableau $ _POST avec is_string ()
Your Common Sense
21
ATTENTION! mysql_real_escape_string() n'est pas infaillible .
eggyal
10
mysql_real_escape_stringest désormais obsolète, ce n'est donc plus une option viable. Il sera supprimé de PHP à l'avenir. Il est préférable de passer à ce que les gens de PHP ou de MySQL recommandent.
juillet 2015
2
Thème: Ne faites pas confiance aux données soumises par l'utilisateur. Tout ce que vous attendez est une donnée poubelle avec des caractères spéciaux ou une logique booléenne, qui devrait elle-même devenir une partie de la requête SQL que vous exécutez. Conservez les valeurs $ _POST en tant que données uniquement, pas en tant que partie SQL.
Bimal Poudel
88

J'ai écrit cette petite fonction il y a plusieurs années:

function sqlvprintf($query, $args)
{
    global $DB_LINK;
    $ctr = 0;
    ensureConnection(); // Connect to database if not connected already.
    $values = array();
    foreach ($args as $value)
    {
        if (is_string($value))
        {
            $value = "'" . mysqli_real_escape_string($DB_LINK, $value) . "'";
        }
        else if (is_null($value))
        {
            $value = 'NULL';
        }
        else if (!is_int($value) && !is_float($value))
        {
            die('Only numeric, string, array and NULL arguments allowed in a query. Argument '.($ctr+1).' is not a basic type, it\'s type is '. gettype($value). '.');
        }
        $values[] = $value;
        $ctr++;
    }
    $query = preg_replace_callback(
        '/{(\\d+)}/', 
        function($match) use ($values)
        {
            if (isset($values[$match[1]]))
            {
                return $values[$match[1]];
            }
            else
            {
                return $match[0];
            }
        },
        $query
    );
    return $query;
}

function runEscapedQuery($preparedQuery /*, ...*/)
{
    $params = array_slice(func_get_args(), 1);
    $results = runQuery(sqlvprintf($preparedQuery, $params)); // Run query and fetch results.   
    return $results;
}

Cela permet d'exécuter des instructions dans une chaîne C # à une ligne. Format comme:

runEscapedQuery("INSERT INTO Whatever (id, foo, bar) VALUES ({0}, {1}, {2})", $numericVar, $stringVar1, $stringVar2);

Il s'échappe en considérant le type de variable. Si vous essayez de paramétrer les noms de table et de colonne, cela échouera car il place chaque chaîne entre guillemets, ce qui n'est pas une syntaxe valide.

MISE À JOUR DE SÉCURITÉ: La str_replaceversion précédente autorisait les injections en ajoutant {#} jetons dans les données utilisateur. Cette preg_replace_callbackversion ne pose pas de problème si le remplacement contient ces jetons.

Calmarius
la source