带有绑定参数的预处理语句优于带有转义/引用参数的插值语句的原因

Reasons for Prepared Statements with Bind Parameters over Interpolated Statements with Escaped / Quoted Parameters

为了防止 SQL 注入,建议使用 准备好的语句 绑定值 。这确保了数据库可以区分 SQL 中的实际逻辑(必须解析、解释和优化)和数据(不需要解释),因此不会解释和执行命令在数据中找到。

另一种实现某种保护的方法是使用转义库,它会解除数据中的重要字符,这样它们就不会被解释。

在我看来,通常建议使用 带有 绑定参数 准备语句 而不是 转义 输入。 具有绑定值的准备好的语句例如在循环中有一些性能优势。

我的问题: 是否有任何安全理由更喜欢 准备好的语句 绑定值 超过 转义 ?如果是,具体原因是什么?

我可能想到的一个原因是 "escaping is tricky" 并且转义库需要与数据库功能完全匹配...还有什么吗?

一个原因是转义只能保护引用的字符串文字。例如(我将使用伪代码,因为您没有引用任何特定的编程语言):

$escapedName = EscapeString("O'Reilly")

$sql = "SELECT * FROM MyTable WHERE name = '$escapedName'"

在上面的例子中,撇号应该被转义,所以它将变成 WHERE name = 'O\'Reilly' 并且因此可以安全地插入到 SQL 查询中而不会导致任何错误。

但是,数字不需要在 SQL 中引用,并且转义包含撇号的字符串不会做正确的事情:

$escapedId = EscapeString("123'456")

$sql = "SELECT * FROM MyTable WHERE id = $escapedId"

这将导致 WHERE id = 123\'456 仍然是一个错误。

您可能会说,"well put the number in single-quotes" 但这并不总是可能的,例如 MySQL 中的 LIMIT 子句需要实数,而不是包含数字的引号字符串。

除了上述问题外,只是写代码比使用转义更容易

例如,您可以编写如下代码:

$sql = "INSERT INTO mytable (col1, col2, col3, col4, col5, col6) 
  VALUES ('" . mysqli_real_escape_string($_POST['col1']) . "', " 
  . $mysqli->real_escape_string($_POST['col2']) . "', '" 
  . $mysqli->real_escape_string($_POST['col3']) . "', '" 
  . $mysqli->real_escape_string($_POST['col4']) . ", '" 
  . $mysqli->real_escape_string($_POST['col5']) . "', '" 
  . $mysqli->real_escape_string($_POST['col6']) . "')";

你能找出错误吗?如果有足够的时间,我相信你可以。但它会减慢您的编码速度,并且在您寻找遗漏的引号字符和其他错误时可能会让您眼睛疲劳。

但是这样写起来容易多了,以后读起来也容易多了:

$sql = "INSERT INTO mytable (col1, col2, col3, col4, col5, col6) 
  VALUES (?, ?, ?, ?, ?, ?)";

查询参数对于更多数据类型是安全的,它们可以帮助您更快地编写代码,减少错误。这是一个巨大的胜利。

整个问题的陈述是为了一个古老的坟墓妄想

escaping which disarms significant chars in the data

坦率地说,这是胡说八道。

  • 没有包罗万象的“重要人物”。一个字符如果被切断可能会对一个查询部分产生毁灭性影响,在另一个查询部分可能像羔羊一样无害。反之亦然。
  • 没有抽象的包罗万象的“数据”。所有查询部分都是不同的,但转义仅适用于一部分。
  • 而且没有任何“使用转义保护”的做法

转义旨在转义SQL 字符串 中的特殊 字符。并且从来没有用于任何保护。这只是一项被严重误解和虐待的技术措施。这就像声称我们在程序中遵循正确的语法只是为了保护。我们遵循正确的语法以使解释器/编译器理解我们的代码。同样在这里。转义用于生成语法正确的 SQL 字符串。作为副作用,这当然是防注射的。但同样——逃跑的任务不是保护。

转义问题 #1 来了:字符串不是查询中使用的唯一数据类型。在任何其他数据文字上使用字符串转义都是通往灾难的直路。

此外,即使对于字符串,转义本质上也是一种可分离措施,仅此一项就构成了一大堆蠕虫,使您的代码容易出现各种人为错误并构成转义问题 #2:

引用我关于此事的文章,Why should I use prepared statements if escaping is safe?:

As you can see, formatting a value for a database is effectively split into two parts, escaping variables and quoting values in the query. And this is where all the magic happens lies the cause for innumerable real life cases of SQL injections.

With your simplified example, where all the code is bound together, it is hard to overlook the proper routine. But in the real life the code is much more complex, consisting of large distinct modules. And escaping is done in one module while quoting in another. Or not. Nobody can tell actually. I'd just trust that this value has been escaped already. Or I will escape it just to be sure, and introduce extra escaping characters in the data. Or I am a new dev, who don't understand the example you posted here, and I was watching a youtube video that said escaping prevents SQL injection. I know the value has been escaped already, so I can put in the query safely. And as it is an integer, why would I waste quotes on it?

Or I know that the data has been escaped already when it was entering the application, so I won't have to escape it during some internal manipulations sometime later (when moving into another table for example). And have a first class second order SQL injection as a result.

Trust me, I've seen all these cases in the wild. Such a separated formatting introduces a total mess and a wast opportunity for injections.

与转义不同,准备好的语句始终确保正确处理查询部分。

虽然我没有关于这个问题的经验证据可以证明它已被使用,但也许值得指出的是,使用混合使用参数变量和常量值的预准备语句将允许数据库查询优化器知道查询的哪些部分会因应用程序而异,哪些部分将保持不变。这可用于查询计划。如果您进入一种将所有数据与查询一起引用的模式,那么优化器将无法猜测哪些部分很可能是固定的,哪些部分是固定的,而不保留查询的所有变体的历史记录并查看差异以找出哪个部分不同。

--we could infer that name will vary and type will not
--but we'd have to analyze all queries sent to work this out
SELECT * FROM person WHERE type = 1 AND name = 'john'
SELECT * FROM person WHERE type = 1 AND name = 'mark'
SELECT * FROM person WHERE type = 1 AND name = 'luke'


--we can easily say that type will vary and name will too
--the previously seen queries would infer differently
SELECT * FROM person WHERE type = @t AND name = @n

我提到过我从来没有读过任何表明它被使用的东西,但是可以读到 sql 服务器做出的称为参数嗅探的决定,它根据它的第一组值缓存和重用计划看到加载到参数中,这可能不会在所有情况下生成最佳查询

当然;这可能会加强您放弃参数的决心,或使用提示使数据库每次都重新计划查询,但最好与服务器一起工作而不是对抗它,并使用技术使其基于公共或尽可能最优的值


即使我们不喜欢根据我们对变量和常量的了解来调整计划,使用准备好的语句至少应该允许数据库编译然后重用该编译工作而不是重做它,减少为 运行 准备声明所必须投入的资源量。

用前端语言术语考虑你的建议:

要更改变量操作的值,您可以通过内存中的简单赋值更改 运行时间值:

sayHello(string name){
  console.print("hello " + name);
}

var name = console.readString(),
sayHello(name);

或者您可以构建一个包含新值的全新脚本文件,将其写入磁盘,在其上调用编译器,退出应用程序的 运行ning 版本并启动新编译的版本:

main(){
  disk.write("sayHello(string name){console.print(\"hello \"" + name +");}", "c:\temp\new.lang");
  launchExe("langcompiler.exe", "c:\temp\new.lang");
  launchExe("c:\temp\new.exe");
}

自己修改程序并重新编译只是为了更改函数调用中使用的值是荒谬的,对吗?

除了数据库服务器对它接收到的每个未参数化 SQL 所做的,除非它付出一些努力来确定它刚刚得到的查询是否与它在 X 分钟前得到的查询大部分相同,除了对于某些数据部分,提取该数据,将其插入 5 分钟前的编译工作中..