如何在PHP中预防SQL注入?
如何在PHP中预防SQL注入?
如果将用户输入原封不动地插入SQL查询中,那么应用程序就会变得容易受到SQL注入攻击,就像以下示例中一样:
$unsafe_variable = $_POST['user_input']; mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");
这是因为用户可以输入类似于value\'); DROP TABLE table;--
这样的内容,那么查询语句就变成了:
INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')
为了防止这种情况的发生,可以采取什么措施呢?
为了使用参数化查询,您需要使用Mysqli或PDO之一。要使用mysqli重新编写示例,我们需要类似以下内容的东西。\n
prepare("INSERT INTO table (column) VALUES (?)"); // "s" means the database expects a string $stmt->bind_param("s", $variable); $stmt->execute();
\n您想要阅读的关键函数是mysqli :: prepare。\n另外,正如其他人建议的那样,您可能会发现使用类似PDO的抽象层更有用/更容易。\n请注意,您提出的情况相当简单,更复杂的情况可能需要更复杂的方法。特别是:\n如果要根据用户输入更改SQL的结构,则参数化查询无法帮助,所需的转义并不包含在mysql_real_escape_string中。在这种情况下,最好通过白名单传递用户的输入,以确保只允许“安全”值通过。
无论您使用哪种数据库,避免SQL注入攻击的正确方法是要将数据与SQL分离,这样数据就保持为数据并且永远不会被SQL解析器解释为命令。可以创建一个具有正确格式化数据部分的SQL语句,但如果您不完全了解细节,您应该始终使用预处理语句和参数化查询。这些是发送到数据库服务器并单独解析的SQL语句,而不涉及任何参数。这样攻击者就无法注入恶意SQL。
您基本上有两个选择:
-
使用PDO(任何支持的数据库驱动程序):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name'); $stmt->execute([ 'name' => $name ]); foreach ($stmt as $row) { // Do something with $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)。
虽然你可以在构造函数的选项中设置charset
,但需要注意的是,“旧版本”的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
解释
你传递给prepare
的SQL语句是由数据库服务器解析和编译的。通过指定参数(在上面的例子中是?
或命名参数: name
),你告诉数据库引擎你想要过滤的地方。然后当你调用execute
时,预处理语句与指定的参数值结合。
这里重要的是,参数值与编译好的语句相结合,而不是一个SQL字符串。SQL注入是通过欺骗脚本在创建要发送到数据库的SQL时包含恶意字符串来工作的。因此,通过将实际的SQL与参数分开发送,可以减轻意外出现意外情况的风险。
使用准备好的语句时发送的任何参数只会被视为字符串(当然,数据库引擎可能会进行一些优化,使参数最终变成数字)。在上面的示例中,如果$name
变量包含'Sarah'; DELETE FROM employees
,结果将只是搜索字符串"'Sarah'; 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'; }