Les instructions préparées par PDO sont-elles suffisantes pour empêcher l'injection SQL?

660

Disons que j'ai un code comme celui-ci:

$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

La documentation PDO indique:

Les paramètres des instructions préparées n'ont pas besoin d'être cités; le conducteur s'en charge pour vous.

Est-ce vraiment tout ce que je dois faire pour éviter les injections SQL? est-ce vraiment si facile?

Vous pouvez assumer MySQL si cela fait une différence. De plus, je ne suis vraiment curieux que d'utiliser des instructions préparées contre l'injection SQL. Dans ce contexte, je ne me soucie pas de XSS ou d'autres vulnérabilités possibles.

Mark Biek
la source
5
meilleure approche 7e numéro réponse stackoverflow.com/questions/134099/…
NullPoiиteя

Réponses:

807

La réponse courte est NON , PDO prépare ne vous défendra pas de toutes les attaques SQL-Injection possibles. Pour certains cas de bord obscurs.

J'adapte cette réponse pour parler de PDO ...

La réponse longue n'est pas si simple. C'est basé sur une attaque démontrée ici .

L'attaque

Commençons donc par montrer l'attaque ...

$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));

Dans certaines circonstances, cela renverra plus d'une ligne. Décortiquons ce qui se passe ici:

  1. Sélection d'un jeu de caractères

    $pdo->query('SET NAMES gbk');

    Pour que cette attaque fonctionne, nous avons besoin du codage que le serveur attend sur la connexion à la fois pour coder 'comme en ASCII ie 0x27 et pour avoir un caractère dont l'octet final est un ASCII \ie 0x5c. Comme il se trouve, il y a 5 ces codages pris en charge par MySQL 5.6 par défaut: big5, cp932, gb2312, gbket sjis. Nous allons sélectionner gbkici.

    Maintenant, il est très important de noter l'utilisation d' SET NAMESici. Cela définit le jeu de caractères SUR LE SERVEUR . Il y a une autre façon de le faire, mais nous y arriverons assez tôt.

  2. La charge utile

    La charge utile que nous allons utiliser pour cette injection commence par la séquence d'octets 0xbf27. Dans gbk, c'est un caractère multi-octets invalide; dans latin1, c'est la chaîne ¿'. Notez que dans latin1 et gbk , 0x27en soi, est un 'caractère littéral .

    Nous avons choisi cette charge utile car, si nous l'appelions addslashes(), nous insérions un ASCII, \c'est 0x5c-à- dire avant le 'caractère. Nous finirions donc avec 0xbf5c27, qui gbkest une séquence de deux caractères: 0xbf5csuivi de 0x27. Ou en d'autres termes, un caractère valide suivi d'un caractère non échappé '. Mais nous n'utilisons pas addslashes(). Passons à l'étape suivante ...

  3. $ stmt-> execute ()

    La chose importante à réaliser ici est que PDO par défaut ne fait pas de véritables instructions préparées. Il les émule (pour MySQL). Par conséquent, PDO construit en interne la chaîne de requête, appelant mysql_real_escape_string()(la fonction MySQL C API) sur chaque valeur de chaîne liée.

    L'appel de l'API C mysql_real_escape_string()diffère de addslashes()par le fait qu'il connaît le jeu de caractères de connexion. Il peut donc effectuer correctement l'échappement pour le jeu de caractères attendu par le serveur. Cependant, jusqu'à présent, le client pense que nous utilisons toujours latin1la connexion, car nous ne l'avons jamais dit autrement. Nous avons dit au serveur que nous utilisons gbk, mais le client pense toujours que c'est le cas latin1.

    Par conséquent, l'appel à mysql_real_escape_string()insère la barre oblique inverse, et nous avons un 'caractère suspendu gratuit dans notre contenu "échappé"! En fait, si nous regardions $vardans le gbkjeu de caractères, nous verrions:

    縗 'OU 1 = 1 / *

    C'est exactement ce dont l'attaque a besoin.

  4. La requête

    Cette partie n'est qu'une formalité, mais voici la requête rendue:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1

Félicitations, vous venez d'attaquer avec succès un programme à l'aide de PDO Prepared Statements ...

La solution simple

Maintenant, il convient de noter que vous pouvez empêcher cela en désactivant les instructions préparées émulées:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Cela se traduira généralement par une véritable instruction préparée (c'est-à-dire que les données sont envoyées dans un paquet distinct de la requête). Cependant, sachez que PDO se repliera silencieusement sur des instructions d'émulation que MySQL ne peut pas préparer nativement: celles qu'il peut sont listées dans le manuel, mais attention à sélectionner la version de serveur appropriée).

La bonne solution

Le problème ici est que nous n'avons pas appelé les API C à la mysql_set_charset()place de SET NAMES. Si nous le faisions, nous serions bien à condition d'utiliser une version de MySQL depuis 2006.

Si vous utilisez une version de MySQL plus tôt, alors un bogue dans mysql_real_escape_string()signifie que les caractères multi - octets invalides tels que ceux de notre charge utile ont été traités comme les octets pour échapper à des fins même si le client avait été correctement informé de l'encodage de connexion et donc cette attaque serait réussir encore. Le bogue a été corrigé dans MySQL 4.1.20 , 5.0.22 et 5.1.11 .

Mais le pire, c'est que PDOl'API C n'a pas été mysql_set_charset()exposée avant la 5.3.6, donc dans les versions précédentes, elle ne peut pas empêcher cette attaque pour chaque commande possible! Il est maintenant exposé en tant que paramètre DSN , qui devrait être utilisé à la place de SET NAMES ...

La grâce salvatrice

Comme nous l'avons dit au début, pour que cette attaque fonctionne, la connexion à la base de données doit être codée à l'aide d'un jeu de caractères vulnérable. utf8mb4n'est pas vulnérable et peut néanmoins prendre en charge tous les caractères Unicode: vous pouvez donc choisir de l'utiliser à la place, mais il n'est disponible que depuis MySQL 5.5.3. Une alternative est utf8, qui n'est pas non plus vulnérable et peut prendre en charge l'ensemble du plan multilingue de base Unicode .

Alternativement, vous pouvez activer le NO_BACKSLASH_ESCAPESmode SQL, qui (entre autres) modifie le fonctionnement de mysql_real_escape_string(). Avec ce mode activé, 0x27sera remplacé par 0x2727plutôt que 0x5c27et donc le processus d'échappement ne peut pas créer de caractères valides dans aucun des encodages vulnérables où ils n'existaient pas auparavant (c'est 0xbf27-à- dire est toujours 0xbf27etc.) - donc le serveur rejettera toujours la chaîne comme invalide . Cependant, voir la réponse de @ eggyal pour une vulnérabilité différente qui peut résulter de l'utilisation de ce mode SQL (mais pas avec PDO).

Exemples sûrs

Les exemples suivants sont sûrs:

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Parce que le serveur attend utf8...

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Parce que nous avons correctement défini le jeu de caractères pour que le client et le serveur correspondent.

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Parce que nous avons désactivé les instructions préparées émulées.

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Parce que nous avons correctement défini le jeu de caractères.

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

Parce que MySQLi fait tout le temps de véritables instructions préparées.

Emballer

Si vous:

  • Utiliser les versions modernes de MySQL (fin 5.1, tous les 5.5, 5.6, etc.) ET le paramètre DSN charset de PDO (en PHP ≥ 5.3.6)

OU

  • N'utilisez pas de jeu de caractères vulnérable pour le codage de connexion (vous utilisez uniquement utf8/ latin1/ ascii/ etc)

OU

  • Activer le NO_BACKSLASH_ESCAPESmode SQL

Vous êtes sûr à 100%.

Sinon, vous êtes vulnérable même si vous utilisez des déclarations préparées par PDO ...

Addenda

J'ai lentement travaillé sur un correctif pour modifier la valeur par défaut afin de ne pas émuler, se prépare pour une future version de PHP. Le problème que je rencontre est que BEAUCOUP de tests se cassent quand je fais ça. Un problème est que les préparations émulées ne lanceront que des erreurs de syntaxe lors de l'exécution, mais les vraies préparations généreront des erreurs lors de la préparation. Cela peut donc causer des problèmes (et cela fait partie de la raison pour laquelle les tests fonctionnent).

ircmaxell
la source
47
C'est la meilleure réponse que j'ai trouvée. Pouvez-vous fournir un lien pour plus de référence?
StaticVariable
1
@nicogawenda: c'était un bug différent. Avant la version 5.0.22, mysql_real_escape_stringne traitait pas correctement les cas où la connexion était correctement définie sur BIG5 / GBK. Donc, en fait, même appeler mysql_set_charset()mysql <5.0.22 serait vulnérable à ce bogue! Donc non, ce post est toujours applicable à 5.0.22 (parce que mysql_real_escape_string est uniquement charset loin des appels de mysql_set_charset(), ce qui est ce que ce post parle de contourner) ...
ircmaxell
1
@progfa Que ce soit le cas ou non, vous devez toujours valider votre entrée sur le serveur avant de faire quoi que ce soit avec les données utilisateur.
Tek
2
Veuillez noter que cela NO_BACKSLASH_ESCAPESpeut également introduire de nouvelles vulnérabilités: stackoverflow.com/a/23277864/1014813
lepix
2
@slevin "OR 1 = 1" est un espace réservé pour tout ce que vous voulez. Oui, il recherche une valeur dans le nom, mais imaginez que la partie "OR 1 = 1" était "UNION SELECT * FROM users". Vous contrôlez maintenant la requête, et en tant que telle, vous pouvez en abuser ...
ircmaxell
515

Les instructions préparées / requêtes paramétrées sont généralement suffisantes pour empêcher l' injection de premier ordre sur cette instruction * . Si vous utilisez SQL dynamique non vérifié ailleurs dans votre application, vous êtes toujours vulnérable à une injection de second ordre .

L'injection de second ordre signifie que les données ont été parcourues une fois dans la base de données avant d'être incluses dans une requête et sont beaucoup plus difficiles à extraire. AFAIK, vous ne voyez presque jamais de vraies attaques de deuxième ordre, car il est généralement plus facile pour les attaquants de s'ingénier socialement, mais vous avez parfois des bogues de deuxième ordre qui surviennent en raison de 'caractères bénins supplémentaires ou similaires.

Vous pouvez effectuer une attaque par injection de second ordre lorsque vous pouvez faire en sorte qu'une valeur soit stockée dans une base de données qui sera ensuite utilisée comme littéral dans une requête. Par exemple, disons que vous entrez les informations suivantes comme nouveau nom d'utilisateur lors de la création d'un compte sur un site Web (en supposant que la base de données MySQL pour cette question):

' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '

S'il n'y a pas d'autres restrictions sur le nom d'utilisateur, une instruction préparée garantirait toujours que la requête intégrée ci-dessus ne s'exécute pas au moment de l'insertion et stocke correctement la valeur dans la base de données. Cependant, imaginez que l'application récupère ultérieurement votre nom d'utilisateur dans la base de données et utilise la concaténation de chaînes pour inclure cette valeur dans une nouvelle requête. Vous pourriez voir le mot de passe de quelqu'un d'autre. Étant donné que les premiers noms de la table des utilisateurs sont généralement des administrateurs, vous avez peut-être également donné la ferme. (Notez également: c'est une raison de plus pour ne pas stocker les mots de passe en texte brut!)

Nous voyons, alors, que les instructions préparées sont suffisantes pour une seule requête, mais en elles-mêmes, elles ne sont pas suffisantes pour se protéger contre les attaques par injection sql dans toute une application, car elles manquent d'un mécanisme pour imposer tous les accès à une base de données dans une application utilise un coffre-fort code. Cependant, utilisées dans le cadre d'une bonne conception d'application - qui peut inclure des pratiques telles que la révision de code ou l'analyse statique, ou l'utilisation d'un ORM, d'une couche de données ou d'une couche de service qui limite le SQL dynamique - les instructions préparées sont le principal outil pour résoudre l'injection SQL problème.Si vous suivez de bons principes de conception d'application, de sorte que votre accès aux données soit séparé du reste de votre programme, il devient facile de faire respecter ou de vérifier que chaque requête utilise correctement le paramétrage. Dans ce cas, l'injection SQL (de premier et de second ordre) est complètement empêchée.


* Il s'avère que MySql / PHP sont (d'accord, étaient) juste stupides à propos de la gestion des paramètres lorsque des caractères larges sont impliqués, et il y a encore un cas rare décrit dans l' autre réponse très votée ici qui peut permettre à l'injection de passer à travers un paramètre requete.

Joel Coehoorn
la source
6
C'est intéressant. Je n'étais pas au courant du 1er ordre contre le 2e ordre. Pouvez-vous nous en dire un peu plus sur le fonctionnement du 2ème ordre?
Mark Biek,
193
Si TOUTES vos requêtes sont paramétrées, vous êtes également protégé contre les injections de 2ème ordre. L'injection de premier ordre oublie que les données utilisateur ne sont pas fiables. L'injection de second ordre oublie que les données de la base de données ne sont pas fiables (car elles proviennent de l'utilisateur à l'origine).
cjm
6
Merci cjm. J'ai également trouvé cet article utile pour expliquer les injections de second ordre: codeproject.com/KB/database/SqlInjectionAttacks.aspx
Mark Biek
49
Ah oui. Mais qu'en est-il de l' injection de troisième ordre . Il faut en être conscient.
troelskn
81
@troelskn qui doit être l'endroit où le développeur est la source de données non fiables
MikeMurko
45

Non, ils ne le sont pas toujours.

Cela dépend si vous autorisez l'entrée de l'utilisateur à être placée dans la requête elle-même. Par exemple:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

serait vulnérable aux injections SQL et l'utilisation d'instructions préparées dans cet exemple ne fonctionnera pas, car l'entrée utilisateur est utilisée comme identifiant, pas comme données. La bonne réponse ici serait d'utiliser une sorte de filtrage / validation comme:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];
$allowedTables = array('users','admins','moderators');
if (!in_array($tableToUse,$allowedTables))    
 $tableToUse = 'users';

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

Remarque: vous ne pouvez pas utiliser PDO pour lier des données qui sortent du DDL (Data Definition Language), c'est-à-dire que cela ne fonctionne pas:

$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');

La raison pour laquelle ce qui précède ne fonctionne pas est parce DESCque ce ASCne sont pas des données . PDO ne peut s'échapper que pour les données . Deuxièmement, vous ne pouvez même pas y mettre de 'guillemets. La seule façon d'autoriser le tri choisi par l'utilisateur est de filtrer manuellement et de vérifier qu'il s'agit de DESCou ASC.

La tour
la source
11
Suis-je en train de manquer quelque chose ici, mais n'est-ce pas tout l'intérêt des instructions préparées pour éviter de traiter sql comme une chaîne? Est-ce que quelque chose comme $ dbh-> prepare ('SELECT * FROM: tableToUse où username =: username'); contourner votre problème?
Rob Forrest du
4
@RobForrest oui vous manque :). Les données que vous liez ne fonctionnent que pour DDL (Data Definition Language). Vous avez besoin de ces citations et d'une bonne évasion. Placer des guillemets pour d'autres parties de la requête la rompt avec une forte probabilité. Par exemple, SELECT * FROM 'table'peut être faux comme il se doit SELECT * FROM `table`ou sans baguettes. Ensuite, certaines choses comme d' ORDER BY DESCDESCvient l'utilisateur ne peuvent pas être simplement échappées. Les scénarios pratiques sont donc plutôt illimités.
Tour
8
Je me demande comment 6 personnes pourraient voter contre un commentaire proposant une utilisation manifestement erronée d'une déclaration préparée. S'ils l'avaient même essayé une fois, ils auraient découvert tout de suite que l'utilisation d'un paramètre nommé à la place d'un nom de table ne fonctionnerait pas.
Félix Gagnon-Grenier
Voici un excellent tutoriel sur PDO si vous voulez l'apprendre. a2znotes.blogspot.in/2014/09/introduction-to-pdo.html
RN Kushwaha
11
Vous ne devez jamais utiliser une chaîne de requête / un corps POST pour choisir la table à utiliser. Si vous n'avez pas de modèles, utilisez au moins a switchpour dériver le nom de la table.
ZiggyTheHamster
29

Oui, c'est suffisant. La façon dont les attaques de type injection fonctionnent, consiste à obtenir un interprète (la base de données) pour évaluer quelque chose, qui aurait dû être des données, comme s'il s'agissait de code. Cela n'est possible que si vous mélangez du code et des données sur le même support (par exemple, lorsque vous créez une requête sous forme de chaîne).

Les requêtes paramétrées fonctionnent en envoyant le code et les données séparément, il ne serait donc jamais possible de trouver un trou dans cela.

Cependant, vous pouvez toujours être vulnérable à d'autres attaques de type injection. Par exemple, si vous utilisez les données dans une page HTML, vous pourriez être sujet à des attaques de type XSS.

troelskn
la source
10
"Jamais" est une façon de le surestimer, au point d'être trompeuse. Si vous n'utilisez pas correctement les instructions préparées, ce n'est pas beaucoup mieux que de ne pas les utiliser du tout. (Bien sûr, une "instruction préparée" à laquelle une entrée utilisateur a été injectée va à l'encontre du but ... mais je l'ai vraiment vu faire. Et les instructions préparées ne peuvent pas gérer les identificateurs (noms de table, etc.) comme paramètres.) Ajouter à cela, certains des pilotes PDO émulent des instructions préparées, et ils peuvent le faire de manière incorrecte (par exemple, en analysant à moitié le SQL). Version courte: ne présumez jamais que c'est aussi simple que cela.
cHao
29

Non ce n'est pas suffisant (dans certains cas spécifiques)! Par défaut, PDO utilise des instructions préparées émulées lors de l'utilisation de MySQL comme pilote de base de données. Vous devez toujours désactiver les instructions préparées émulées lorsque vous utilisez MySQL et PDO:

$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Une autre chose qui devrait toujours être faite est de définir le bon encodage de la base de données:

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

Voir également cette question connexe: Comment puis-je empêcher l'injection SQL en PHP?

Notez également que cela concerne uniquement le côté base de données des éléments que vous devrez toujours surveiller vous-même lors de l'affichage des données. Par exemple, en utilisant à htmlspecialchars()nouveau avec le bon style d'encodage et de citation.

PeeHaa
la source
14

Personnellement, je voudrais toujours exécuter une forme d'assainissement sur les données en premier, car vous ne pouvez jamais faire confiance à l'entrée de l'utilisateur, cependant lorsque vous utilisez la liaison des paramètres / espaces réservés, les données entrées sont envoyées au serveur séparément dans l'instruction sql, puis liées ensemble. La clé ici est que cela lie les données fournies à un type spécifique et à une utilisation spécifique et élimine toute possibilité de modifier la logique de l'instruction SQL.

JimmyJ
la source
1

Même si vous voulez empêcher le frontal d'injection SQL, en utilisant des contrôles html ou js, vous devez considérer que les contrôles frontaux sont "contournables".

Vous pouvez désactiver js ou modifier un modèle avec un outil de développement frontal (intégré à Firefox ou Chrome de nos jours).

Donc, afin d'empêcher l'injection SQL, il serait juste de nettoyer le backend de la date d'entrée dans votre contrôleur.

Je voudrais vous suggérer d'utiliser la fonction PHP native filter_input () afin d'assainir les valeurs GET et INPUT.

Si vous voulez aller de l'avant avec la sécurité, pour les requêtes de base de données sensibles, je voudrais vous suggérer d'utiliser une expression régulière pour valider le format des données. preg_match () vous aidera dans ce cas! Mais attention! Le moteur Regex n'est pas si léger. Utilisez-le uniquement si nécessaire, sinon les performances de votre application diminueront.

La sécurité a un coût, mais ne gâchez pas vos performances!

Exemple simple:

si vous voulez vérifier si une valeur reçue de GET est un nombre, moins de 99 si (! preg_match ('/ [0-9] {1,2} /')) {...} est plus lourd de

if (isset($value) && intval($value)) <99) {...}

Ainsi, la réponse finale est: "Non! Les déclarations préparées par PDO n'empêchent pas toutes sortes d'injection SQL"; Il n'empêche pas les valeurs inattendues, juste une concaténation inattendue

tireur d'élite
la source
5
Vous confondez l'injection SQL avec quelque chose d'autre qui rend votre réponse complètement hors de propos
Your Common Sense