为什么存储过程和准备好的语句是防止 SQL 注入通过 mysql 真正的转义字符串()函数的首选现代方法

信息安全 php sql注入 数据库 mysql
2021-08-24 04:49:24

为什么存储过程和准备好的语句是防止 SQL 注入函数的首选现代方法mysql_real_escape_string()

4个回答

SQL注入的问题本质上是逻辑和数据的混合,应该是数据的东西被当作逻辑处理。准备好的语句和参数化查询实际上通过将逻辑与数据完全分离,从源头上解决了这个问题,因此注入几乎是不可能的。字符串转义函数实际上并没有解决问题,但它试图通过转义某些字符来防止伤害。使用字符串转义函数,无论它有多好,总是担心可能出现的边缘情况可能允许未转义的字符通过。

正如@Sebastian 的评论中提到的,另一个重要的一点是,在使用准备好的语句编写应用程序时更容易保持一致。使用准备好的语句在架构上与普通的旧方式不同;您可以通过绑定参数(例如,PHP 的PDOStatement::bindParam )来构建语句,而不是字符串连接,然后是单独的执行语句(PDOStatement::execute)。但是mysql_real_escape_string(),除了执行查询之外,您还需要记住首先调用该函数。如果您的应用程序有 1000 个执行 SQL 查询的端点,但您甚至忘记调用mysql_real_escape_string()其中一个,或者调用不当,您的整个数据库都可能处于开放状态。

我认为这里的主要问题是为什么字符串转义不如其他方法好。

答案是某些边缘情况允许注入即使被逃脱,也可以通过。Stack Overflow 有几个例子

虽然您可以安全地防止 SQLi 转义用户输入,但重要的是要注意这可能并不总是足够的。在这个可怕的例子中,执行成功的 SQL 注入永远不需要引号,尽管有转义:

<?php
/*
  This is an example of bad practice due to several reasons.
  This code shall never be used in production.
*/

$post_id = mysql_real_escape_string($_GET['id']);

$qry = mysql_query("SELECT title,text FROM posts WHERE id=$post_id");

$row = mysql_fetch_array($qry);

echo '<h1>'.$row['title'].'</h1>';
echo $row['text'];

现在如果一个vises会发生什么.php?id=-1+UNION+ALL+SELECT+1,version()让我们看看查询是如何进行的:

SELECT title,text FROM posts WHERE id=-1 UNION ALL SELECT 1,version()

当然还有其他方法可以修复它(即使用引号和转义或整数转换),但使用准备好的语句是一种不太容易丢失这些东西并让驱动程序关心如何在查询中包含用户输入的方法(甚至因为,虽然这个例子看起来很容易修复,但存在多级 SQL 注入,基本上包括将部分 SQL 查询保存到数据库,因为知道未来将使用来自数据库的数据作为另一个查询)。

安全总比后悔好。

因为您正在做相同数量的工作以获得更好的安全性

针对 PHP 提到的一个常见比喻是mysqli_real_escape_string()(看看它有多长!它们不能与命名法保持一致吗?),但大多数人没有意识到 PHP API只是调用 MySQL API它在做什么?

此函数创建一个合法的 SQL 字符串以在 SQL 语句中使用

换句话说,当您使用此函数时,您是在要求 MySQL 清理该值,以便在 SQL 中使用它是“安全的”。您已经要求数据库在这方面为您工作。整个过程是这样的

  1. 逃避不受信任的数据
  2. 组装最终的SQL查询(即将转义的数据放入SQL)
  3. 解析查询
  4. 执行查询

当您使用准备好的语句时,您是在告诉您的数据库它需要以不同的顺序执行这些操作

  1. 使用占位符解析查询
  2. 发送数据以填充占位符,同时指定数据类型
  3. 执行查询

因为我们是单独发送数据,所以我们做的工作量与转义相同,但我们得到的好处是我们的 SQL没有不受信任的数据因此,引擎永远不会将提供的数据与 SQL 指令混淆。

这里还有一个隐藏的好处。您可能没有意识到,一旦第 1 步完成,您可以一遍又一遍地执行第 2 步和第 3 步,前提是它们都需要执行相同的查询,只是使用不同的数据。

$prep = $mysqli->prepare('INSERT INTO visits_log(visitor_name, visitor_id) VALUES(?, ?)');
$prep->bind_param('si', $visitor_name, $visitor_id); // this function passes by reference
foreach($_POST['visitor_list'] as $visitor_id => $visitor_name) $prep->execute();

这个查询的好处是只需要循环数据并一遍又一遍地发送,而不是每次都增加解析的开销。