如果用户输入未经修改就插入到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;--')

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


当前回答

警告:本答案中描述的方法仅适用于非常特定的场景,并不安全,因为SQL注入攻击不仅仅依赖于能够注入X=Y。

如果攻击者试图通过PHP的$_GET变量或URL的查询字符串侵入表单,如果他们不安全,您将能够抓住他们。

RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+)
RewriteRule ^(.*) ^/track.php

因为1=1、2=2、1=2、2=1、1+1=2等……是攻击者SQL数据库的常见问题。也许它也被许多黑客应用程序使用。

但您必须小心,不能从站点重写安全查询。上面的代码为您提供了一个提示,可以重写或重定向(这取决于您)将特定的动态查询字符串黑客入侵到一个页面中,该页面将存储攻击者的IP地址,甚至是他们的COOKIES、历史记录、浏览器或任何其他敏感信息,因此您可以稍后通过禁用他们的帐户或联系当局来处理他们。

其他回答

要使用参数化查询,需要使用Mysqli或PDO。要用mysqli重写示例,我们需要以下内容。

<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("server", "username", "password", "database_name");

$variable = $_POST["user-input"];
$stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");
// "s" means the database expects a string
$stmt->bind_param("s", $variable);
$stmt->execute();

你想在那里读到的关键函数是mysqli::prepare。

此外,正如其他人所建议的,您可能会发现使用PDO之类的东西来提升抽象层是有用的/更容易的。

请注意,您询问的案例相当简单,更复杂的案例可能需要更复杂的方法。特别地:

如果您希望根据用户输入更改SQL的结构,参数化查询将不会有帮助,并且mysql_real_ascape_string不包含所需的转义。在这种情况下,最好通过白名单传递用户的输入,以确保只允许通过“安全”值。

使用PDO和准备好的查询。

($conn是PDO对象)

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

无论使用哪个数据库,避免SQL注入攻击的正确方法都是将数据与SQL分离,这样数据就不会被SQL解析器解释为命令。可以使用正确格式化的数据部分创建SQL语句,但如果您不完全了解详细信息,则应始终使用准备好的语句和参数化查询。这些是SQL语句,与任何参数分开发送到数据库服务器并由其解析。这样,攻击者就不可能注入恶意SQL。

你基本上有两种选择来实现这一点:

使用PDO(适用于任何受支持的数据库驱动程序):$stmt=$pdo->prepare('SELECT*FROM employees WHERE name=:name');$stmt->execute(['name'=>$name]);foreach($stm作为$row){//用$row做点什么}使用MySQLi(用于MySQL):

由于PHP 8.2+,我们可以使用execute_query(),它在一个方法中准备、绑定参数和执行SQL语句:

$result = $dbConnection->execute_query('SELECT * FROM employees WHERE name = ?', [$name]);

while ($row = $result->fetch_assoc()) {
    // Do something with $row
}

PHP8.1之前:

$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
$stmt->execute();

$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
    // Do something with $row
}

如果要连接到MySQL以外的数据库,则可以参考一个特定于驱动程序的第二个选项(例如,PostgreSQL的pg_prepare()和pg_execute())。PDO是通用选项。


正确设置连接

PDO

注意,当使用PDO访问MySQL数据库时,默认情况下不会使用真正准备好的语句。要解决此问题,必须禁用对已准备语句的模拟。使用PDO创建连接的示例如下:

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8mb4', 'user', 'password');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

在上面的示例中,错误模式并不是绝对必要的,但建议添加它。这样PDO将通过抛出PDOException通知您所有MySQL错误。

然而,第一行setAttribute()是必需的,它告诉PDO禁用模拟的准备语句并使用真正的准备语句。这可以确保在将语句和值发送到MySQL服务器之前,PHP不会对其进行解析(使攻击者没有机会注入恶意SQL)。

尽管您可以在构造函数的选项中设置字符集,但需要注意的是,PHP的“旧”版本(5.3.6之前)会默默忽略DSN中的字符集参数。

Mysqli公司

对于mysqli,我们必须遵循相同的程序:

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // error reporting
$dbConnection = new mysqli('127.0.0.1', 'username', 'password', 'test');
$dbConnection->set_charset('utf8mb4'); // charset

解释

传递给准备的SQL语句由数据库服务器解析和编译。通过指定参数(在上面的示例中为?或命名参数,如:name),您可以告诉数据库引擎要过滤的位置。然后,当您调用execute时,准备好的语句将与您指定的参数值组合在一起。

这里重要的一点是,参数值与编译语句组合,而不是SQL字符串。SQL注入的工作原理是,当脚本创建要发送到数据库的SQL时,诱使脚本包含恶意字符串。因此,通过将实际的SQL与参数分开发送,可以限制出现意外情况的风险。

在使用准备好的语句时发送的任何参数都将被视为字符串(当然,数据库引擎可能会进行一些优化,因此参数也可能以数字结尾)。在上面的示例中,如果$name变量包含“Sarah”;DELETE FROM employees(从员工中删除)结果只需搜索字符串“‘Arah’;DELETE FROM employees”,您不会得到空表。

使用准备好的语句的另一个好处是,如果您在同一会话中多次执行同一语句,那么它只会被解析和编译一次,从而提高了速度。

哦,既然您询问了如何为插入操作,这里有一个示例(使用PDO):

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');

$preparedStatement->execute([ 'column' => $unsafeValue ]);

准备好的语句可以用于动态查询吗?

虽然您仍然可以为查询参数使用准备好的语句,但动态查询本身的结构不能参数化,某些查询功能也不能参数化。

对于这些特定的场景,最好使用白名单过滤器来限制可能的值。

// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}

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

我正面临这个问题,但我认为我以非常复杂的方式解决了它——黑客避免使用引号的方式。我将其与模拟的准备语句结合使用。我使用它来防止各种可能的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');

从安全角度来看,我倾向于存储过程(MySQL从5.0开始就支持存储过程),其优点是-

大多数数据库(包括MySQL)允许将用户访问限制为执行存储过程。细粒度安全访问控制有助于防止特权攻击升级。这防止了受损的应用程序能够直接针对数据库运行SQL。它们从应用程序中提取原始SQL查询,因此应用程序可以获得的数据库结构信息较少。这使得人们更难理解数据库的底层结构并设计合适的攻击。它们只接受参数,因此参数化查询的优势就在这里。当然,IMO仍然需要清理输入,尤其是在存储过程中使用动态SQL时。

缺点是-

它们(存储过程)很难维护,而且往往会很快繁殖。这使得管理它们成为一个问题。它们不太适合动态查询——若构建它们是为了接受动态代码作为参数,那个么许多优点就被否定了。