SQL 查询参数化如何工作?

Posted

技术标签:

【中文标题】SQL 查询参数化如何工作?【英文标题】:How does SQL query parameterisation work? 【发布时间】:2010-11-18 18:52:25 【问题描述】:

我觉得问这个有点傻,因为我似乎是世界上唯一一个不明白的人,但不管怎样,还是这样吧。我将使用 Python 作为示例。当我使用原始 SQL 查询(我通常使用 ORM)时,我使用参数化,就像这个使用 SQLite 的示例:

方法一:

username = "wayne"
query_params = (username)
cursor.execute("SELECT * FROM mytable WHERE user=?", query_params)

我知道这是可行的,而且我知道这是通常推荐的方法。一个 SQL 注入漏洞的方法来做同样的事情是这样的:

方法B:

username = "wayne"
cursor.execute("SELECT * FROM mytable WHERE user='%s'" % username)

据我所知,我了解 SQL 注入,如 this Wikipedia article 中所述。我的问题很简单:方法 A 与方法 B 有何不同?为什么方法 A 的最终结果与方法 B 不一样?我假设 cursor.execute() 方法(Python 的 DB-API 规范的一部分)负责正确地转义和对输入进行类型检查,但这从未在任何地方明确说明。这就是在这种情况下的所有参数化吗?对我来说,当我们说“参数化”时,这意味着“字符串替换”,比如 %-formatting。不对吗?

【问题讨论】:

我会将它与存储过程一起使用,但单独使用它是一个好问题。读了一点后我发现是因为下面说的 Calsbeek。参数化查询将“wayne;drop table users”作为整个用户名来查找,而不是将其视为进一步的指令集。此页面上的最后一条评论告诉我:taylorza.blogspot.com/2009/04/… 【参考方案1】:

当您进行文本替换时(如您的方法 B),您必须警惕引号等,因为服务器将获取单个文本,并且它必须确定值的结束位置。

使用参数化语句 OTOH,DB 服务器按原样获取语句,不带参数。使用简单的二进制安全协议,将值作为不同的数据块发送到服务器。因此,您的程序不必在值周围加上引号,当然,值本身是否已经有引号也没关系。

类比是源代码和编译代码:在您的方法 B 中,您正在构建过程的源代码,因此您必须确保严格遵循语言语法。使用方法 A,您首先构建和编译一个过程,然后(在您的示例中,紧接其后)调用该过程,并将您的值作为参数。当然,内存中的值不受语法限制。

嗯...这并不是一个真正的类比,这确实是幕后发生的事情(大致)。

【讨论】:

这个类比帮助我了解了情况,大致。 +1【参考方案2】:

这里只是一个警告。 这 ?语法可以正常工作并正确转义字符串中嵌入的单引号或双引号。

但是我发现了一种情况,它不起作用。我有一列跟踪“n.n.n”形式的版本字符串,例如“1.2.3” 格式似乎会导致错误,因为直到第二个“.”之前它看起来都是实数。例如:

   rec = (some_value, '1.2.3')
   sql = ''' UPDATE some_table
              SET some_column=?
              WHERE version=? '''
    cur = self.conn.cursor()
    cur.execute(sql, rec)

失败并出现错误“提供的绑定数量不正确。当前语句使用 1,提供了 2 个。”

这工作正常:

   vers = '1.2.3'
   rec = (some_value)
   sql = ''' UPDATE some_table
              SET some_column=?
              WHERE version='%s' ''' % (vers)
    cur = self.conn.cursor()
    cur.execute(sql, rec)

【讨论】:

【参考方案3】:

使用参数化查询是一种很好的方法来逃避和防止注入到数据库客户端库的任务。它会在用“?”替换字符串之前进行转义。这是在客户端库中完成的,位于数据库服务器之前。

如果您正在运行 mysql,请打开 SQL 日志,并尝试一些参数化查询,您将看到 MySQL 服务器正在接收完全替换的查询,没有“?”在其中,但 MySQL 客户端库已经为您转义了“参数”中的任何引号。

如果您使用方法 B 仅替换字符串,则 "s 不会自动转义。

协同作用,使用 MySQL,您可以提前准备参数化查询,然后在以后重复使用准备好的语句。当你准备一个查询时,MySQL 会解析它并给你一个准备好的语句——一些 MySQL 可以理解的解析表示。每次使用prepared statement,不仅可以防范注入,还可以避免再次解析查询的开销。

而且,如果您真的想保证安全,您可以修改您的 DB 访问/ORM 层,以便 1)Web 服务器代码只能使用准备好的语句,以及 2)您只能在您的 Web 服务器启动之前准备语句。然后,即使您的 Web 应用程序被黑客入侵(例如通过缓冲区溢出漏洞利用),黑客仍然只能使用准备好的语句,但仅此而已。为此,您需要监禁您的 Web 应用程序,并且只允许通过您的 DB 访问/ORM 层访问数据库。

【讨论】:

【参考方案4】:

当您通过 SQL Server 提交查询时,它首先检查过程缓存。如果它发现 somequery 完全相等,那么他将使用相同的计划,而不是重新编译查询,只是替换占位符(变量)但在服务器(db)端。

检查系统表 master.dbo.syscacheobjects,并进行一些测试,以便您对这个主题有更多了解。

【讨论】:

虽然这是特定于 SQL Server 的,但大多数数据库引擎都会这样做。不过,不确定 SQLite(提到的引擎)是否有。 这是一个完整的概念,除了理解参数化查询的作用以及它们为什么提供安全优势。 抱歉有任何误解,这就是我最初开始理解为什么我必须替换我的字符串替换查询,观察这个系统表,并跟踪我的查询是否会破坏服务器上的过程缓存的方式. @Cheekysoft:安全优势并不是我们可能想要使用参数化查询的唯一原因,我们可以通过重用查询计划来获得性能优势。【参考方案5】:

参数化查询实际上并不进行字符串替换。如果你使用字符串替换,那么 SQL 引擎实际上会看到一个看起来像

的查询
SELECT * FROM mytable WHERE user='wayne'

如果您使用 ? 参数,则 SQL 引擎会看到类似的查询

SELECT * FROM mytable WHERE user=<some value>

这意味着它甚至在看到字符串“wayne”之前,就可以完全解析查询并理解查询的作用。它将“wayne”粘贴到它自己的查询表示中,而不是描述查询的 SQL 字符串中。因此,SQL 注入是不可能的,因为我们已经通过了流程的 SQL 阶段。

(以上是概括性的,但或多或​​少传达了这个想法。)

【讨论】:

那么如果他有wayne;drop table accounts,会出现什么样的错误呢?只是没有结果? @johnny:它会找到来自mytable 的所有内容,其中userwayne;drop table accounts。它会拉回 Little Bobby Tables 的实际记录。 @johny:对,没有结果。因为这是一个有效的值,即使它很丑陋。客户端和服务器之间的二进制安全协议不关心引号、分号或类似的东西。 谢谢约翰,这就是我正在寻找的解释!你给了我一个“ooooooooooooh I seeeeeeeeeeeeeeeeee”的时刻,范式转变发生了:) 我没有意识到我不应该考虑字符串,而是考虑进一步发生的事情。 “它将“wayne”粘贴到自己的查询表示中”是什么意思?

以上是关于SQL 查询参数化如何工作?的主要内容,如果未能解决你的问题,请参考以下文章

这个参数化查询如何防止 SQL 注入?

参数化查询如何帮助防止 SQL 注入?

参数化查询如何帮助防止 SQL 注入?

sql参数化查询

如何编写参数化的 SQL 查询?

参数化 SQL 查询