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

Posted

技术标签:

【中文标题】带有绑定参数的预处理语句优于带有转义/引用参数的插值语句的原因【英文标题】:Reasons for Prepared Statements with Bind Parameters over Interpolated Statements with Escaped / Quoted Parameters 【发布时间】:2020-06-07 13:49:41 【问题描述】:

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

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

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

我的问题:是否有任何安全理由更喜欢 prepared statementsbind values 而不是 escaping?如果是,具体原因是什么?

我可能会想到的一个原因是“转义很棘手”并且转义库需要与数据库功能完全匹配......还有什么?

【问题讨论】:

准备好的语句总是安全的,而转义则容易出错。好的开发者会做正确的逃避;其余 80% 将被击中或错过。 owasp.org/www-project-cheat-sheets/cheatsheets/… 【参考方案1】:

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

$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 仍然是一个错误。

您可能会说,“将数字放在单引号中”,但这并不总是可行的,例如 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 (?, ?, ?, ?, ?, ?)";

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

【讨论】:

【参考方案2】:

整个问题的陈述都是为了一个古老的坟墓错觉

转义会解除数据中的重要字符

坦率地说,是一派胡言。

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

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

这里出现了转义问题 #1:字符串不是查询中唯一使用的数据类型。在任何其他数据文字上使用字符串转义是一条直接的灾难之路。

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

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

如您所见,格式化数据库的值实际上分为两部分,转义变量和在查询中引用值。这就是所有奇迹发生的原因,这就是现实生活中无数案例 SQL 注入的原因。

在您的简化示例中,所有代码都绑定在一起,因此很难忽视正确的例程。但在现实生活中,代码要复杂得多,由不同的大模块组成。转义是在一个模块中完成的,同时在另一个模块中引用。或不。其实谁也说不清。我只是相信这个值已经被转义了。或者我将转义它只是为了确定,并在数据中引入额外的转义字符。或者我是一个新开发人员,不理解您在此处发布的示例,并且我正在观看一个说转义防止 SQL 注入的 youtube 视频。我知道这个值已经被转义了,所以我可以安全地输入查询。因为它是一个整数,我为什么要在它上面浪费引号?

或者我知道数据在进入应用程序时已经被转义,所以我不必在稍后的一些内部操作中转义它(例如,当移动到另一个表时)。结果是一级二级 SQL 注入。

相信我,我在野外见过所有这些案例。这种分离的格式会导致一团糟,并且浪费了注入的机会。

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

【讨论】:

感谢您的详细回答。它提供了很多见解。有些短语对我来说似乎有点不必要的情感负担。但除此之外,问题是为什么不应该在安全上下文中使用它。答案不能只是“因为”…… ...您是否有任何证据证明声明“并且从未用于任何保护。”?这将非常有帮助! AFAIK 在准备好的带有绑定值的语句之前已经存在时间和数据库,甚至是一件事……所以至少在那个时候,我认为转义是故意用作安全措施的。它也列在 OWASP 安全工具列表中(但作为最后的手段)。 我相信你需要付出更多的努力来寻求理解。但我相信有一天你会做对的。 我无法找到任何可靠的指示来表明哪个数据库首先引入了参数化查询以及何时引入,但 Oracle 在1992. 当然,oracle 在此之前存在并且可能在那时 SQL 注入黑客攻击(如果它是一件事)很可能已被清理防止,但“我们并不总是有参数”的论点在面对已经使用了大约 30 年【参考方案3】:

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

--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 server 做出的称为参数嗅探的决定,它根据它看到加载到参数中的第一组值来缓存和重用计划,这可能不会在所有情况下都生成最佳查询

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


即使我们根据我们对变量和常量的了解来调整计划并不可爱,使用准备好的语句至少应该允许数据库编译然后重用编译工作而不是重做它,降低了准备运行语句所需的资源量。

用前端语言来考虑你的提案:

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

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


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

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

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 分钟前将其插入到编译工作中..

【讨论】:

“想想你的提议” ...我无意提出提议。所以我不确定你指的是什么。 您的提议似乎是“在准备好的语句上使用转义语句”,不是吗?

以上是关于带有绑定参数的预处理语句优于带有转义/引用参数的插值语句的原因的主要内容,如果未能解决你的问题,请参考以下文章

真正的转义字符串与绑定参数

CMD命令中的参数带有双引号,如何转义双引号?

CMD命令中的参数带有双引号,如何转义双引号?

JDBC驱动程序为准备好的语句转义参数?

Shell脚本:转义序列字符在检索带有转义序列的变量值时如何表现?

带有绑定参数的 MarkupExtension