博士TL;
Mysql_real_escape_string()将不提供任何保护(并可能进一步munge你的数据),如果:
MySQL的NO_BACKSLASH_ESCAPES SQL模式被启用(它可能是,除非你每次连接时显式地选择另一个SQL模式);而且
您的SQL字符串文字使用双引号“字符”来引用。
这被归档为bug #72458,并已在MySQL v5.7.6中修复(参见下面的“可取之处”一节)。
这是另一个模糊的边缘情况!!
为了向@ircmaxell的精彩回答致敬(真的,这应该是奉承而不是抄袭!),我将采用他的格式:
这次袭击
从演示开始…
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');
这将返回测试表中的所有记录。解剖:
Selecting an SQL Mode
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
As documented under String Literals:
There are several ways to include quote characters within a string:
A “'” inside a string quoted with “'” may be written as “''”.
A “"” inside a string quoted with “"” may be written as “""”.
Precede the quote character by an escape character (“\”).
A “'” inside a string quoted with “"” needs no special treatment and need not be doubled or escaped. In the same way, “"” inside a string quoted with “'” needs no special treatment.
If the server's SQL mode includes NO_BACKSLASH_ESCAPES, then the third of these options—which is the usual approach adopted by mysql_real_escape_string()—is not available: one of the first two options must be used instead. Note that the effect of the fourth bullet is that one must necessarily know the character that will be used to quote the literal in order to avoid munging one's data.
The Payload
" OR 1=1 --
The payload initiates this injection quite literally with the " character. No particular encoding. No special characters. No weird bytes.
mysql_real_escape_string()
$var = mysql_real_escape_string('" OR 1=1 -- ');
Fortunately, mysql_real_escape_string() does check the SQL mode and adjust its behaviour accordingly. See 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);
}
Thus a different underlying function, escape_quotes_for_mysql(), is invoked if the NO_BACKSLASH_ESCAPES SQL mode is in use. As mentioned above, such a function needs to know which character will be used to quote the literal in order to repeat it without causing the other quotation character from being repeated literally.
However, this function arbitrarily assumes that the string will be quoted using the single-quote ' character. See 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++= '\'';
}
So, it leaves double-quote " characters untouched (and doubles all single-quote ' characters) irrespective of the actual character that is used to quote the literal! In our case $var remains exactly the same as the argument that was provided to mysql_real_escape_string()—it's as though no escaping has taken place at all.
The Query
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
Something of a formality, the rendered query is:
SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
正如我那位博学的朋友所说:恭喜你,你刚刚成功地使用mysql_real_escape_string()攻击了一个程序……
坏
Mysql_set_charset()无法提供帮助,因为这与字符集无关;mysqli::real_escape_string()也不能,因为这只是对同一个函数的不同包装。
这个问题(如果还不明显的话)是,对mysql_real_escape_string()的调用不能知道用哪个字符引用文字,因为这是留给开发人员以后决定的。因此,在NO_BACKSLASH_ESCAPES模式中,这个函数实际上无法安全地转义使用任意引号使用的每个输入(至少,不能不将不需要转义的字符加倍,从而破坏数据)。
丑陋的
It gets worse. NO_BACKSLASH_ESCAPES may not be all that uncommon in the wild owing to the necessity of its use for compatibility with standard SQL (e.g. see section 5.3 of the SQL-92 specification, namely the <quote symbol> ::= <quote><quote> grammar production and lack of any special meaning given to backslash). Furthermore, its use was explicitly recommended as a workaround to the (long since fixed) bug that ircmaxell's post describes. Who knows, some DBAs might even configure it to be on by default as means of discouraging use of incorrect escaping methods like addslashes().
另外,新连接的SQL模式由服务器根据其配置设置(SUPER用户可以随时更改);因此,为了确定服务器的行为,您必须在连接后始终显式地指定所需的模式。
可取之处
只要您总是显式地设置SQL模式不包括NO_BACKSLASH_ESCAPES,或者使用单引号字符引用MySQL字符串字面量,这个错误就不会出现它丑陋的头:分别escape_quotes_for_mysql()将不会被使用,或者它关于需要重复哪些引号字符的假设是正确的。
出于这个原因,我建议任何使用NO_BACKSLASH_ESCAPES的人也启用ANSI_QUOTES模式,因为它将强制习惯性地使用单引号字符串字面值。请注意,这并不能阻止使用双引号文字时的SQL注入——它只是降低了发生这种情况的可能性(因为正常的、非恶意的查询将会失败)。
在PDO中,它的等效函数PDO::quote()和它的预处理语句模拟器都调用mysql_handle_quoter()——它确实做到了这一点:它确保转义的文字以单引号引用,因此您可以确定PDO始终不会受到这个错误的影响。
从MySQL v5.7.6开始,这个错误已经修复。参见变更日志:
Functionality Added or Changed
Incompatible Change: A new C API function, mysql_real_escape_string_quote(), has been implemented as a replacement for mysql_real_escape_string() because the latter function can fail to properly encode characters when the NO_BACKSLASH_ESCAPES SQL mode is enabled. In this case, mysql_real_escape_string() cannot escape quote characters except by doubling them, and to do this properly, it must know more information about the quoting context than is available. mysql_real_escape_string_quote() takes an extra argument for specifying the quoting context. For usage details, see mysql_real_escape_string_quote().
Note
Applications should be modified to use mysql_real_escape_string_quote(), instead of mysql_real_escape_string(), which now fails and produces an CR_INSECURE_API_ERR error if NO_BACKSLASH_ESCAPES is enabled.
References: See also Bug #19211994.
安全的例子
结合ircmaxell解释的错误,下面的例子是完全安全的(假设你使用的MySQL版本高于4.1.20、5.0.22、5.1.11;或者没有使用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');
...因为我们已经显式地选择了不包括NO_BACKSLASH_ESCAPES的SQL模式。
mysql_set_charset($charset);
$var = mysql_real_escape_string("' OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
...因为我们用单引号引用了字符串字面量。
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);
...因为PDO预处理语句不受此漏洞的影响(ircmaxell也是如此,前提是您使用的PHP≥5.3.6并且在DSN中正确设置了字符集;或者准备语句模拟已禁用)。
$var = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");
...因为PDO的quote()函数不仅转义字面量,而且还引用它(单引号'字符);注意,在这种情况下,为了避免ircmaxell的错误,您必须使用PHP≥5.3.6,并在DSN中正确设置字符集。
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
...因为MySQLi准备的语句是安全的。
结束
因此,如果你:
使用本机准备语句
OR
使用MySQL v5.7.6或更高版本
OR
除了使用ircmaxell总结中的一种解决方案外,至少使用以下其中一种:
PDO;
单引号字符串字面量;或
显式设置的SQL模式,不包括NO_BACKSLASH_ESCAPES
...那么您应该是完全安全的(除了字符串转义范围之外的漏洞)。