如果用户输入未经修改就插入到SQL查询中,则应用程序很容易受到SQL注入的攻击,如下例所示:

$unsafe_variable = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

这是因为用户可以输入类似值的内容);DROP TABLE表;--,并且查询变为:

INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')

可以采取什么措施防止这种情况发生?


当前回答

不推荐的警告:这个答案的示例代码(与问题的示例代码一样)使用了PHP的MySQL扩展,该扩展在PHP 5.5.0中被弃用,在PHP 7.0.0中被完全删除。安全警告:此答案不符合安全最佳实践。转义不足以防止SQL注入,请改用准备好的语句。使用以下概述的策略,风险自负。(此外,在PHP7中删除了mysql_real_ascape_string()。)

参数化查询AND输入验证是一种方法。尽管使用了mysql_real_ascape_string(),但在许多情况下可能会发生SQL注入。

这些示例易受SQL注入攻击:

$offset = isset($_GET['o']) ? $_GET['o'] : 0;
$offset = mysql_real_escape_string($offset);
RunQuery("SELECT userid, username FROM sql_injection_test LIMIT $offset, 10");

or

$order = isset($_GET['o']) ? $_GET['o'] : 'userid';
$order = mysql_real_escape_string($order);
RunQuery("SELECT userid, username FROM sql_injection_test ORDER BY `$order`");

在这两种情况下,都不能使用“”来保护封装。

来源:意外的SQL注入(当转义不够时)

其他回答

正如你所看到的,人们建议你最多使用准备好的陈述。这并没有错,但如果每个进程只执行一次查询,则会有轻微的性能损失。

我正面临这个问题,但我认为我以非常复杂的方式解决了它——黑客避免使用引号的方式。我将其与模拟的准备语句结合使用。我使用它来防止各种可能的SQL注入攻击。

我的方法:

如果您希望输入是整数,请确保它真的是整数。在像PHP这样的变量类型语言中,这一点非常重要。例如,您可以使用这个非常简单但功能强大的解决方案:sprintf(“SELECT 1,2,3 FROM table WHERE 4=%u”,$input);如果你对整数十六进制有任何期望,如果你对它十六进制,你将完全逃避所有输入。在C/C++中有一个名为mysql_hex_string()的函数,在PHP中可以使用bin2hex()。不用担心转义字符串的大小是其原始长度的2倍,因为即使使用mysql_real_ascape_string,PHP也必须分配相同的容量((2*input_length)+1),这是相同的。这种十六进制方法通常在传输二进制数据时使用,但我认为没有理由不对所有数据使用它来防止SQL注入攻击。请注意,您必须在数据前面加上0x,或者改用MySQL函数UNHEX。

例如,查询:

SELECT password FROM users WHERE name = 'root';

将变成:

SELECT password FROM users WHERE name = 0x726f6f74;

or

SELECT password FROM users WHERE name = UNHEX('726f6f74');

Hex是完美的逃脱。无法注射。

UNHEX函数与0x前缀之间的差异

评论中有一些讨论,所以我最后想说清楚。这两种方法非常相似,但在某些方面略有不同:

0x前缀只能用于char、varchar、text、block、binary等数据列。此外,如果要插入空字符串,则其使用有点复杂。您必须将其完全替换为“”,否则会出现错误。

UNHEX()适用于任何列;你不必担心空字符串。


十六进制方法通常用作攻击

注意,这种十六进制方法通常被用作SQL注入攻击,其中整数就像字符串一样,并且只使用mysql_real_aescape_string进行转义。然后可以避免使用引号。

例如,如果你只是这样做:

"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])

攻击很容易给你注射。考虑从脚本返回的以下注入代码:

SELECT ... WHERE id = -1 UNION ALL SELECT table_name FROM information_schema.tables;

现在只提取表结构:

SELECT ... WHERE id = -1 UNION ALL SELECT column_name FROM information_schema.column WHERE table_name = __0x61727469636c65__;

然后只需选择所需的数据。是不是很酷?

但如果可注入站点的编码器将其十六进制,则不可能进行注入,因为查询将如下所示:

SELECT ... WHERE id = UNHEX('2d312075...3635');

使用PDO和准备好的查询。

($conn是PDO对象)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();

不推荐的警告:这个答案的示例代码(与问题的示例代码一样)使用了PHP的MySQL扩展,该扩展在PHP 5.5.0中被弃用,在PHP 7.0.0中被完全删除。安全警告:此答案不符合安全最佳实践。转义不足以防止SQL注入,请改用准备好的语句。使用以下概述的策略,风险自负。(此外,在PHP7中删除了mysql_real_ascape_string()。)

使用PDO和MYSQLi是防止SQL注入的好方法,但如果您真的想使用MySQL函数和查询,最好使用

mysql_real_reape_string

$unsafe_variable = mysql_real_escape_string($_POST['user_input']);

还有更多的功能可以防止这种情况:比如识别-如果输入是字符串、数字、字符或数组,那么有很多内置函数可以检测这种情况。此外,最好使用这些函数来检查输入数据。

is_string(is_string)

$unsafe_variable = (is_string($_POST['user_input']) ? $_POST['user_input'] : '');

是数字(_N)

$unsafe_variable = (is_numeric($_POST['user_input']) ? $_POST['user_input'] : '');

使用这些函数来检查mysql_real_aescape_string中的输入数据要好得多。

安全警告:此答案不符合安全最佳实践。转义不足以防止SQL注入,请改用准备好的语句。

SQL语句中转义特殊字符的一些准则。

不要使用MySQL。此扩展已弃用。请改用MySQLi或PDO。

MySQLi

对于手动转义字符串中的特殊字符,可以使用mysqli_real_escape_string函数。除非使用mysqli_set_charset设置了正确的字符集,否则该函数将无法正常工作。

例子:

$mysqli = new mysqli('host', 'user', 'password', 'database');
$mysqli->set_charset('charset');

$string = $mysqli->real_escape_string($string);
$mysqli->query("INSERT INTO table (column) VALUES ('$string')");

要使用准备好的语句自动转义值,请使用mysqli_prepare和mysqli_stmt_bind_param,其中必须提供相应绑定变量的类型以进行适当的转换:

例子:

$stmt = $mysqli->prepare("INSERT INTO table (column1, column2) VALUES (?,?)");

$stmt->bind_param("is", $integer, $string);

$stmt->execute();

无论您使用prepared语句还是mysqli_real_escape_string,您都必须知道正在使用的输入数据的类型。

因此,如果使用准备好的语句,则必须指定mysqli_stmt_bind_param函数的变量类型。

正如名字所说,mysqli_real_escape_string的使用是为了转义字符串中的特殊字符,因此它不会使整数安全。此函数的目的是防止破坏SQL语句中的字符串,以及它可能对数据库造成的损坏。如果使用得当,mysqli_realescape_string是一个有用的函数,尤其是与sprintf结合使用时。

例子:

$string = "x' OR name LIKE '%John%";
$integer = '5 OR id != 0';

$query = sprintf( "SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 5

$integer = '99999999999999999999';
$query = sprintf("SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 2147483647

我建议使用PDO(PHP数据对象)运行参数化SQL查询。

这不仅可以防止SQL注入,还可以加快查询速度。

而且,通过使用PDO而不是mysql_、mysqli_和pgsql_函数,可以使应用程序从数据库中抽象出一点,这是很少需要切换数据库提供程序的情况。