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:
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
, gbk
et sjis
. Nous allons sélectionner gbk
ici.
Maintenant, il est très important de noter l'utilisation d' SET NAMES
ici. 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.
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
, 0x27
en 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 gbk
est une séquence de deux caractères: 0xbf5c
suivi 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 ...
$ 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 latin1
la 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 $var
dans le gbk
jeu de caractères, nous verrions:
縗 'OU 1 = 1 / *
C'est exactement ce dont l'attaque a besoin.
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 PDO
l'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. utf8mb4
n'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_ESCAPES
mode SQL, qui (entre autres) modifie le fonctionnement de mysql_real_escape_string()
. Avec ce mode activé, 0x27
sera remplacé par 0x2727
plutôt que 0x5c27
et 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 0xbf27
etc.) - 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_ESCAPES
mode 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).