引言:mysql_real_escape_string()真的值得担忧吗?
\n\n您可能听过PHP开发者的一句老话:“mysql_real_escape_string()能防御所有SQL注入”。事实果真如此吗?在2024年,当PDO和预处理语句已成为标准时,这个话题似乎有些过时。但仍有成千上万的开发者在维护旧项目,其中mysql_real_escape_string()是唯一的安全防线。
在Stack Overflow上,“能够绕过mysql_real_escape_string()的SQL注入”这个问题获得了数十万次浏览。为什么?因为答案并不显而易见。本文将剖析转义失效的真实场景,并对比不同的防护方法。如果您使用PHP开发,请仔细阅读。
\n\nmysql_real_escape_string()的工作原理
\n\nmysql_real_escape_string()函数(来自现已弃用的mysql_*扩展)会根据当前连接的字符集对字符串中的特殊字符进行转义。它会在以下字符前添加反斜杠:'、"、\\、\\n、\\r、NULL和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\n看起来这很有效。但其中存在一些微妙的问题。
\n\n场景1:字符集问题(GBK / Big5)
\n\n最著名的绕过方法是利用多字节字符集,例如GBK(中文)或Big5。如果连接设置为GBK,而您传入字符串%bf%27,就会发生神奇的事情。
在GBK中,字节0xbf不是一个完整的字符。当mysql_real_escape_string()遇到'(0x27)时,它会添加一个反斜杠0x5c。结果序列变为:0xbf + 0x5c + 0x27。但在GBK中,0xbf5c是一个有效的字符(例如汉字“縗”)。结果反斜杠被“吃掉”,而引号仍然未被转义!
<?php\n// 设置GBK字符集\nmysql_query("SET NAMES 'GBK'", $connection);\n$input = "%bf%27"; // 字节:0xbf 0x27\n$escaped = mysql_real_escape_string($input, $connection);\n// 结果:0xbf5c27 -> 在GBK中为"縗'"(引号保留)\n$query = "SELECT * FROM users WHERE id = '$escaped'";\n// 实际执行:SELECT * FROM users WHERE id = '縗''\n// 现在可以注入:' OR 1=1 -- \n?>\n\n结论:如果连接的字符集与数据的字符集不匹配,mysql_real_escape_string()可能被绕过。这个漏洞正是著名的PHP Bug #47168中描述的问题。
场景2:数字字段的使用
\n\n转义仅适用于字符串。如果您将数据插入不带引号的数字字段,mysql_real_escape_string()将无济于事,因为值不会被转义——它会原样插入。
<?php\n$id = mysql_real_escape_string($_GET['id']); // 假设为"1 OR 1=1"\n$query = "SELECT * FROM users WHERE id = $id"; // 不带引号!\n// 危险:SELECT * FROM users WHERE id = 1 OR 1=1\n?>\n\n即使您对字符串进行了转义,在数字上下文中也毫无意义——因为没有引号,所以可以注入任何SQL代码。
\n\n结论:转义不能替代数据类型检查。对于数字,请使用intval()或ctype_digit()。
场景3:LIKE查询与通配符
\n\nmysql_real_escape_string()会转义'和\\,但不会处理