Introduction : faut-il craindre mysql_real_escape_string() ?
\n\nVous avez sûrement entendu le vieux dicton des développeurs PHP : « mysql_real_escape_string() sauve de toutes les injections SQL ». Est-ce vraiment le cas ? En 2024, alors que PDO et les prepared statements sont devenus la norme, ce sujet semble archaïque. Mais des milliers de développeurs héritent encore d'anciens projets où mysql_real_escape_string() est la seule ligne de défense.
Sur Stack Overflow, la question « Injection SQL contournant mysql_real_escape_string() » a recueilli des centaines de milliers de vues. Pourquoi ? Parce que la réponse n'est pas évidente. Dans cet article, nous analyserons des scénarios réels où l'échappement ne sauve pas, et comparerons différentes approches de protection. Si vous codez en PHP — lisez attentivement.
\n\nComment fonctionne mysql_real_escape_string()
\n\nLa fonction mysql_real_escape_string() (de l'extension mysql_*, désormais dépréciée) échappe les caractères spéciaux dans une chaîne, en tenant compte de l'encodage actuel de la connexion. Elle ajoute une barre oblique inverse devant les caractères : ', ", \\, \\n, \\r, NULL et Control-Z.
<?php\n$connection = mysql_connect('localhost', 'user', 'pass');\nmysql_select_db('test', $connection);\n$unsafe = "O'Brien";\n$safe = mysql_real_escape_string($unsafe); // O\\'Brien\n$query = "SELECT * FROM users WHERE name = '$safe'";\n?>\n\nCela semble fonctionner. Mais il y a des nuances.
\n\nScénario 1 : Problème d'encodage (GBK / Big5)
\n\nLa méthode de contournement la plus connue consiste à utiliser des encodages multi-octets, comme GBK (chinois) ou Big5. Si la connexion est définie sur GBK et que vous transmettez la chaîne %bf%27 — la magie opère.
En GBK, l'octet 0xbf n'est pas un caractère complet. Lorsque mysql_real_escape_string() rencontre ' (0x27), il ajoute une barre oblique inverse 0x5c. On obtient la séquence : 0xbf + 0x5c + 0x27. Mais en GBK, 0xbf5c est un seul caractère valide (par exemple, l'idéogramme « 縗 »). En conséquence, la barre oblique est « avalée » et le guillemet reste non protégé !
<?php\n// Définition de l'encodage GBK\nmysql_query("SET NAMES 'GBK'", $connection);\n$input = "%bf%27"; // Octets : 0xbf 0x27\n$escaped = mysql_real_escape_string($input, $connection);\n// Résultat : 0xbf5c27 -> en GBK c'est "縗'" (le guillemet est resté)\n$query = "SELECT * FROM users WHERE id = '$escaped'";\n// En réalité exécuté : SELECT * FROM users WHERE id = '縗''\n// On peut maintenant injecter : ' OR 1=1 -- \n?>\n\nConclusion : Si l'encodage de la connexion ne correspond pas à l'encodage des données, mysql_real_escape_string() peut être contourné. C'est exactement ce bogue qui a été décrit dans le célèbre rapport de bogue PHP #47168.
Scénario 2 : Utilisation de champs numériques
\n\nL'échappement ne fonctionne qu'avec les chaînes. Si vous insérez des données dans un champ numérique sans guillemets, mysql_real_escape_string() ne sert à rien, car la valeur n'est pas échappée — elle est simplement insérée telle quelle.
<?php\n$id = mysql_real_escape_string($_GET['id']); // Supposons "1 OR 1=1"\n$query = "SELECT * FROM users WHERE id = $id"; // Sans guillemets !\n// Danger : SELECT * FROM users WHERE id = 1 OR 1=1\n?>\n\nMême si vous échappez la chaîne, dans un contexte numérique cela n'a pas de sens — il n'y a pas de guillemets, donc on peut injecter n'importe quel code SQL.
\n\nConclusion : L'échappement ne remplace pas la vérification du type de données. Pour les nombres, utilisez intval() ou ctype_digit().
Scénario 3 : Requêtes LIKE et caractères wildcard
\n\nmysql_real_escape_string() échappe ' et \\, mais ne touche pas aux caractères