是否有SQL注入的可能性,即使使用mysql_real_escape_string()函数?

考虑这个示例情况。SQL在PHP中是这样构造的:

$login = mysql_real_escape_string(GetFromPost('login'));
$password = mysql_real_escape_string(GetFromPost('password'));

$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";

我听很多人对我说,这样的代码仍然是危险的,即使使用mysql_real_escape_string()函数也可能被破解。但我想不到任何可能的利用?

经典注射剂是这样的:

aaa' OR 1=1 --

不要工作。

你知道有什么可能的注入可以通过上面的PHP代码吗?


Well, there's nothing really that can pass through that, other than % wildcard. It could be dangerous if you were using LIKE statement as attacker could put just % as login if you don't filter that out, and would have to just bruteforce a password of any of your users. People often suggest using prepared statements to make it 100% safe, as data can't interfere with the query itself that way. But for such simple queries it probably would be more efficient to do something like $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);


考虑以下查询:

$iId = mysql_real_escape_string("1 OR 1=1");    
$sSql = "SELECT * FROM table WHERE id = $iId";

Mysql_real_escape_string()不会保护你。 事实上,您在查询中的变量周围使用单引号(' ')可以防止这种情况发生。以下也是一个选项:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";

简短的回答是肯定的,有一种方法可以绕过mysql_real_escape_string()。 #非常模糊的边缘情况!!

要回答这个问题并不容易。它是基于这里演示的攻击。

这次袭击

所以,让我们从展示攻击开始…

mysql_query('SET NAMES gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

在某些情况下,它会返回多于一行。让我们来分析一下这里发生了什么:

Selecting a Character Set mysql_query('SET NAMES gbk'); For this attack to work, we need the encoding that the server's expecting on the connection both to encode ' as in ASCII i.e. 0x27 and to have some character whose final byte is an ASCII \ i.e. 0x5c. As it turns out, there are 5 such encodings supported in MySQL 5.6 by default: big5, cp932, gb2312, gbk and sjis. We'll select gbk here. Now, it's very important to note the use of SET NAMES here. This sets the character set ON THE SERVER. If we used the call to the C API function mysql_set_charset(), we'd be fine (on MySQL releases since 2006). But more on why in a minute... The Payload The payload we're going to use for this injection starts with the byte sequence 0xbf27. In gbk, that's an invalid multibyte character; in latin1, it's the string ¿'. Note that in latin1 and gbk, 0x27 on its own is a literal ' character. We have chosen this payload because, if we called addslashes() on it, we'd insert an ASCII \ i.e. 0x5c, before the ' character. So we'd wind up with 0xbf5c27, which in gbk is a two character sequence: 0xbf5c followed by 0x27. Or in other words, a valid character followed by an unescaped '. But we're not using addslashes(). So on to the next step... mysql_real_escape_string() The C API call to mysql_real_escape_string() differs from addslashes() in that it knows the connection character set. So it can perform the escaping properly for the character set that the server is expecting. However, up to this point, the client thinks that we're still using latin1 for the connection, because we never told it otherwise. We did tell the server we're using gbk, but the client still thinks it's latin1. Therefore the call to mysql_real_escape_string() inserts the backslash, and we have a free hanging ' character in our "escaped" content! In fact, if we were to look at $var in the gbk character set, we'd see: 縗' OR 1=1 /* Which is exactly what the attack requires. The Query This part is just a formality, but here's the rendered query: SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1

恭喜你,你刚刚成功地使用mysql_real_escape_string()攻击了一个程序…

情况变得更糟。PDO默认使用MySQL模拟准备好的语句。这意味着在客户端,它基本上通过mysql_real_escape_string()(在C库中)执行一个sprintf,这意味着以下将导致成功的注入:

$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

现在,值得注意的是,你可以通过禁用模拟预处理语句来防止这种情况:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

这通常会导致一个真正的预处理语句(即数据从查询中以单独的数据包发送)。但是,请注意PDO将会无声地退回到模拟MySQL不能在本地准备的语句:手册中列出了那些它可以准备的语句,但要注意选择适当的服务器版本)。

丑陋的

我在一开始就说过,如果我们使用mysql_set_charset('gbk')而不是SET NAMES gbk,我们就可以防止这一切。如果你使用的是2006年以后的MySQL版本,这是正确的。

如果您使用的是较早的MySQL版本,那么mysql_real_escape_string()中的一个错误意味着无效的多字节字符(例如我们的有效负载中的那些字符)被当作单字节进行转义,即使客户端已经正确地通知了连接编码,因此这种攻击仍然会成功。该错误已在MySQL 4.1.20, 5.0.22和5.1.11中修复。

但最糟糕的是,PDO直到5.3.6才公开mysql_set_charset()的C API,所以在以前的版本中,它不能防止每个可能的命令都受到这种攻击! 它现在被公开为一个DSN参数。

可取之处

正如我们在开始时所说,要使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。utf8mb4不容易受到攻击,但可以支持所有Unicode字符:所以你可以选择使用它,但它只在MySQL 5.5.3之后可用。另一种替代方法是utf8,它也不容易受到攻击,并且可以支持整个Unicode基本多语言平面。

Alternatively, you can enable the NO_BACKSLASH_ESCAPES SQL mode, which (amongst other things) alters the operation of mysql_real_escape_string(). With this mode enabled, 0x27 will be replaced with 0x2727 rather than 0x5c27 and thus the escaping process cannot create valid characters in any of the vulnerable encodings where they did not exist previously (i.e. 0xbf27 is still 0xbf27 etc.)—so the server will still reject the string as invalid. However, see @eggyal's answer for a different vulnerability that can arise from using this SQL mode.

安全的例子

下面的例子是安全的:

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");

因为服务器期望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");

因为我们已经正确地设置了字符集,所以客户端和服务器匹配。

$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 /*"));

因为我们已经关闭了模拟的准备语句。

$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 /*"));

因为我们已经正确地设置了字符集。

$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();

因为MySQLi一直在执行真准备语句。

结束

如果你:

mysql_set_charset() / $mysqli->set_charset() / PDO的DSN字符集参数(PHP≥5.3.6)

OR

不要使用易受攻击的字符集进行连接编码(你只使用utf8 / latin1 / ascii / etc)

你100%安全。

否则,即使使用mysql_real_escape_string(),也很容易受到攻击…


博士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

...那么您应该是完全安全的(除了字符串转义范围之外的漏洞)。