TL; DR
mysql_real_escape_string()
volonté fournira aucune protection (et pourrait en outre communiquer vos données) si:
Le NO_BACKSLASH_ESCAPES
mode SQL de MySQL est activé (ce qui pourrait être le cas, sauf si vous sélectionnez explicitement un autre mode SQL à chaque connexion ); et
vos littéraux de chaîne SQL sont indiqués entre guillemets "
.
Cela a été classé comme bug # 72458 et a été corrigé dans MySQL v5.7.6 (voir la section intitulée " La grâce qui sauve ", ci-dessous).
Ceci est un autre, (peut-être moins?) CAS DE BORD obscur !!!
En hommage à l'excellente réponse @ ircmaxell (vraiment, c'est censé être de la flatterie et non du plagiat!), J'adopterai son format:
L'attaque
Commençant par une démonstration ...
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"'); // could already be set
$var = mysql_real_escape_string('" OR 1=1 -- ');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
Cela renverra tous les enregistrements de la test
table. Une dissection:
Sélection d'un mode SQL
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
Comme documenté sous String Literals :
Il existe plusieurs façons d'inclure des guillemets dans une chaîne:
Un « '
» à l'intérieur d'une chaîne entre guillemets « '
» peut s'écrire « ''
».
Un « "
» à l'intérieur d'une chaîne entre guillemets « "
» peut s'écrire « ""
».
Faites précéder le caractère de citation d'un caractère d'échappement (« \
»).
Un " '
" à l'intérieur d'une chaîne entre guillemets " "
" ne nécessite aucun traitement spécial et n'a pas besoin d'être doublé ou échappé. De la même manière, " "
" à l'intérieur d'une chaîne entre guillemets " '
" ne nécessite aucun traitement spécial.
Si le mode SQL du serveur inclut NO_BACKSLASH_ESCAPES
, alors la troisième de ces options - qui est l'approche habituelle adoptée par mysql_real_escape_string()
- n'est pas disponible: l'une des deux premières options doit être utilisée à la place. Notez que l'effet de la quatrième puce est que l'on doit nécessairement connaître le caractère qui sera utilisé pour citer le littéral afin d'éviter de fusionner ses données.
La charge utile
" OR 1=1 --
La charge utile lance cette injection littéralement avec le "
personnage. Pas d'encodage particulier. Pas de caractères spéciaux. Pas d'octets bizarres.
mysql_real_escape_string ()
$var = mysql_real_escape_string('" OR 1=1 -- ');
Heureusement, mysql_real_escape_string()
vérifie le mode SQL et ajuste son comportement en conséquence. Voir libmysql.c
:
ulong STDCALL
mysql_real_escape_string(MYSQL *mysql, char *to,const char *from,
ulong length)
{
if (mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES)
return escape_quotes_for_mysql(mysql->charset, to, 0, from, length);
return escape_string_for_mysql(mysql->charset, to, 0, from, length);
}
Ainsi, une fonction sous-jacente différente,, escape_quotes_for_mysql()
est invoquée si le NO_BACKSLASH_ESCAPES
mode SQL est utilisé. Comme mentionné ci-dessus, une telle fonction doit savoir quel caractère sera utilisé pour citer le littéral afin de le répéter sans provoquer la répétition littérale de l'autre caractère de citation.
Cependant, cette fonction suppose arbitrairement que la chaîne sera citée en utilisant le '
caractère guillemet simple . Voir charset.c
:
/*
Escape apostrophes by doubling them up
// [ deletia 839-845 ]
DESCRIPTION
This escapes the contents of a string by doubling up any apostrophes that
it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in
effect on the server.
// [ deletia 852-858 ]
*/
size_t escape_quotes_for_mysql(CHARSET_INFO *charset_info,
char *to, size_t to_length,
const char *from, size_t length)
{
// [ deletia 865-892 ]
if (*from == '\'')
{
if (to + 2 > to_end)
{
overflow= TRUE;
break;
}
*to++= '\'';
*to++= '\'';
}
Ainsi, il laisse les "
caractères entre guillemets intacts (et double tous les '
caractères entre guillemets ) quel que soit le caractère réel utilisé pour citer le littéral ! Dans notre cas, il $var
reste exactement le même que l'argument qui a été fourni à mysql_real_escape_string()
- c'est comme si aucune fuite n'avait eu lieu du tout .
La requête
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
Quelque chose d'une formalité, la requête rendue est:
SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
Comme l'a dit mon confrère: félicitations, vous venez d'attaquer avec succès un programme en utilisant mysql_real_escape_string()
...
Le mauvais
mysql_set_charset()
ne peut pas aider, car cela n'a rien à voir avec les jeux de caractères; pas plus mysqli::real_escape_string()
, car c'est juste un wrapper différent autour de cette même fonction.
Le problème, s'il n'est pas déjà évident, est que l'appel à mysql_real_escape_string()
ne peut pas savoir avec quel caractère le littéral sera cité, car il appartient au développeur de décider plus tard. Donc, en NO_BACKSLASH_ESCAPES
mode, il n'y a littéralement aucun moyen que cette fonction puisse échapper en toute sécurité à chaque entrée pour une utilisation avec des guillemets arbitraires (au moins, non sans doubler les caractères qui ne nécessitent pas de doubler et donc de fusionner vos données).
Le moche
Ça s'empire. NO_BACKSLASH_ESCAPES
peut ne pas être si rare dans la nature en raison de la nécessité de son utilisation pour la compatibilité avec le SQL standard (par exemple, voir la section 5.3 de la spécification SQL-92 , à savoir la <quote symbol> ::= <quote><quote>
production de grammaire et le manque de signification particulière donnée à la barre oblique inverse). De plus, son utilisation a été explicitement recommandée comme solution de contournement au bogue (corrigé depuis longtemps) décrit par la publication d'ircmaxell. Qui sait, certains administrateurs de base de données peuvent même le configurer pour qu'il soit activé par défaut afin de décourager l'utilisation de méthodes d'échappement incorrectes comme addslashes()
.
De plus, le mode SQL d'une nouvelle connexion est défini par le serveur en fonction de sa configuration (qu'un SUPER
utilisateur peut modifier à tout moment); ainsi, pour être certain du comportement du serveur, vous devez toujours spécifier explicitement le mode souhaité après la connexion.
La grâce salvatrice
Tant que vous définissez toujours explicitement le mode SQL pour qu'il n'inclue pas NO_BACKSLASH_ESCAPES
ou ne cite pas les littéraux de chaîne MySQL en utilisant le caractère guillemet simple, ce bogue ne peut pas escape_quotes_for_mysql()
afficher sa tête laide: respectivement , ne sera pas utilisé, ou son hypothèse sur les caractères guillemet nécessitant une répétition sera être correct.
Pour cette raison, je recommande que toute personne utilisant NO_BACKSLASH_ESCAPES
également active le ANSI_QUOTES
mode, car cela forcera l'utilisation habituelle des littéraux de chaîne entre guillemets simples. Notez que cela n'empêche pas l'injection SQL dans le cas où des littéraux entre guillemets sont utilisés - cela réduit simplement la probabilité que cela se produise (car les requêtes normales et non malveillantes échoueraient).
Dans PDO, sa fonction équivalente PDO::quote()
et son émulateur d'instructions préparé font appel à mysql_handle_quoter()
- ce qui fait exactement cela: il garantit que le littéral échappé est cité entre guillemets simples, de sorte que vous pouvez être certain que PDO est toujours à l'abri de ce bogue.
Depuis MySQL v5.7.6, ce bug a été corrigé. Voir le journal des modifications :
Fonctionnalité ajoutée ou modifiée
Exemples sûrs
Pris ensemble avec le bogue expliqué par ircmaxell, les exemples suivants sont entièrement sûrs (en supposant que l'on utilise MySQL plus tard que 4.1.20, 5.0.22, 5.1.11; ou que l'on n'utilise pas un codage de connexion GBK / Big5) :
mysql_set_charset($charset);
mysql_query("SET SQL_MODE=''");
$var = mysql_real_escape_string('" OR 1=1 /*');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
... car nous avons explicitement sélectionné un mode SQL qui ne comprend pas NO_BACKSLASH_ESCAPES
.
mysql_set_charset($charset);
$var = mysql_real_escape_string("' OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
... parce que nous citons notre chaîne littérale avec des guillemets simples.
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);
... parce que les instructions préparées par PDO sont immunisées contre cette vulnérabilité (et ircmaxell aussi, à condition que vous utilisiez PHP ≥5.3.6 et que le jeu de caractères ait été correctement défini dans le DSN; ou que l'émulation des instructions préparées ait été désactivée) .
$var = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");
... parce que la quote()
fonction de PDO échappe non seulement au littéral, mais le cite également (en guillemets simples '
); notez que pour éviter le bogue d'ircmaxell dans ce cas, vous devez utiliser PHP≥5.3.6 et avoir correctement défini le jeu de caractères dans le DSN.
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
... car les instructions préparées par MySQLi sont sûres.
Emballer
Ainsi, si vous:
- utiliser des instructions natives préparées
OU
- utiliser MySQL v5.7.6 ou version ultérieure
OU
... alors vous devriez être complètement en sécurité (vulnérabilités en dehors de la portée de la chaîne s'échappant).