准备好的语句对 SQL 注入是否 100% 安全?

信息安全 sql注入
2021-08-21 04:41:18

假设所有用户提供的参数都作为查询绑定参数传递,准备好的语句实际上对 SQL 注入 100% 安全吗?

每当我看到人们在 StackOverflow 上使用旧mysql_函数时(遗憾的是,这太频繁了),我通常会告诉人们准备好的语句是 SQL 注入安全措施的 Chuck Norris(或 Jon Skeet)。

但是,我实际上从未见过任何明确声明“这是 100% 安全”的文档。我对它们的理解是它们将查询语言和参数一直分离到服务器的前门,然后将它们视为单独的实体。

我在这个假设中正确吗?

2个回答

保证 100% 安全不受 SQL 注入影响?不会得到它(从我这里)。

原则上,您的数据库(或与数据库交互的您的语言中的库)可以以不安全的方式实现带有绑定参数的准备好的语句,容易受到某种高级攻击,例如利用缓冲区溢出或在用户中具有空终止字符-提供的字符串等(你可以争辩说这些类型的攻击不应该被称为 SQL 注入,因为它们根本不同;但这只是语义)。

我从来没有听说过这些针对现场真实数据库的预处理语句的攻击,强烈建议使用绑定参数来防止 SQL 注入。没有绑定参数或输入卫生,进行 SQL 注入是微不足道的。仅输入卫生设施,通常可以在卫生设施周围找到一个不起眼的漏洞。

使用绑定参数,您的 SQL 查询执行计划可以在不依赖用户输入的情况下提前计算出来,这应该使 SQL 注入不可能(因为任何插入的引号、注释符号等都只插入到已经编译的 SQL 语句中)。

反对使用准备好的语句的唯一理由是您希望数据库根据实际查询优化执行计划。大多数数据库在给出完整查询时都足够聪明,可以制定最佳执行计划;例如,如果查询返回表的很大一部分,它会想要遍历整个表以查找匹配项;而如果它只获取几条记录,您可以进行基于索引的搜索[1]

编辑: 回应两个批评(评论有点太长了):

首先,正如其他人指出的那样,是的,每个支持准备好的语句和绑定参数的关系数据库都不一定在不查看绑定参数的值的情况下预编译准备好的语句。许多数据库通常会这样做,但数据库也可以在确定执行计划时查看绑定参数的值。这不是问题,因为带有分离的绑定参数的预准备语句的结构使数据库可以轻松地将 SQL 语句(包括 SQL 关键字)与绑定参数中的数据(其中绑定参数中的任何内容都不会)区分开来解释为 SQL 关键字)。在从变量和 SQL 关键字混合的字符串连接中构造 SQL 语句时,这是不可能的。

其次,正如另一个答案指出的那样,在程序中的某个时间点调用 SQL 语句时使用绑定参数将在进行顶级调用时安全地防止 SQL 注入。但是,如果您在应用程序的其他地方存在 SQL 注入漏洞(例如,在您存储和运行在数据库中的用户定义函数中,您不安全地编写这些函数以通过字符串连接构造 SQL 查询)。

例如,如果在您的应用程序中您编写了如下伪代码:

sql_stmt = "SELECT create_new_user(?, ?)"
params = (email_str, hashed_pw_str)
db_conn.execute_with_params(sql_stmt, params)

在应用程序级别运行此 SQL 语句时,不能有 SQL 注入。但是,如果用户定义的数据库函数被不安全地编写(使用 PL/pgSQL 语法):

CREATE FUNCTION create_new_user(email_str, hashed_pw_str) RETURNS VOID AS
$$
DECLARE
   sql_str TEXT;
BEGIN
     sql_str := 'INSERT INTO users VALUES (' || email_str || ', ' || hashed_pw_str || ');'
     EXECUTE sql_str;
END;
$$
LANGUAGE plpgsql;

那么您将容易受到 SQL 注入攻击,因为它执行通过字符串连接构造的 SQL 语句,该语句将 SQL 语句与包含用户定义变量值的字符串混合在一起。

也就是说,除非您试图不安全(通过字符串连接构造 SQL 语句),否则以安全的方式编写用户定义的语句会更自然,例如:

CREATE FUNCTION create_new_user(email_str, hashed_pw_str) RETURNS VOID AS
$$
BEGIN
     INSERT INTO users VALUES (email_str, hashed_pw_str);
END;
$$
LANGUAGE plpgsql;

此外,如果您真的觉得需要在用户定义的函数中从字符串组成 SQL 语句,即使在用户定义的函数中,您仍然可以使用与存储过程/绑定参数相同的方式将数据变量从 SQL 语句中分离出来。例如在PL/pgSQL中:

CREATE FUNCTION create_new_user(email_str, hashed_pw_str) RETURNS VOID AS
$$
DECLARE
   sql_str TEXT;
BEGIN
     sql_str := 'INSERT INTO users VALUES($1, $2)'
     EXECUTE sql_str USING email_str, hashed_pw_str;
END;
$$
LANGUAGE plpgsql;

因此,使用准备好的语句对 SQL 注入是安全的,只要您不只是在其他地方做不安全的事情(即通过字符串连接构造 SQL 语句)。

100% 安全吗?差远了。绑定参数(以语句方式或其他方式准备)可以有效地防止 100% 的一SQL 注入漏洞(假设没有数据库错误和健全的实现)。他们绝不会阻止其他课程。请注意,PostgreSQL(我选择的数据库)具有将参数绑定到临时语句的选项,如果您不需要这些语句的某些功能,则可以节省有关准备好的语句的往返行程。

您必须了解,许多大型、复杂的数据库本身就是程序。这些程序的复杂性差别很大,SQL 注入是必须在编程例程内部注意的事情。这样的例程包括触发器、用户定义的函数、存储过程等。这些东西如何从应用程序级别交互并不总是很明显,因为许多优秀的 dba 在应用程序访问级别和存储级别之间提供了某种程度的抽象。

使用绑定参数,解析查询树,然后,至少在 PostgreSQL 中,查看数据以进行计划。该计划被执行。使用准备好的语句,计划被保存,因此您可以一遍又一遍地使用不同的数据重新执行相同的计划(这可能是您想要的,也可能不是您想要的)。但关键是,使用绑定参数,参数不能将任何内容注入解析树。所以这类 SQL 注入问题得到了妥善处理。

但是现在我们需要记录谁在写什么到一张表中,所以我们添加了触发器和用户定义的函数来封装这些触发器的逻辑。这些提出了新的问题。如果您在这些中有任何动态 SQL,那么您必须担心那里的 SQL 注入。他们写入的表可能有自己的触发器,等等。类似地,函数调用可能会调用另一个查询,该查询可能会调用另一个函数调用,依此类推。这些中的每一个都是独立于主树规划的。

这意味着如果我运行一个带有绑定参数的查询,foo'; drop user postgres; --那么它不能直接暗示顶级查询树并导致它添加另一个命令来删除 postgres 用户。但是,如果此查询直接或不直接调用另一个函数,则可能在某个地方,某个函数将易受攻击,并且 postgres 用户将被删除。绑定参数没有为辅助查询提供保护。这些辅助查询需要确保它们也尽可能使用绑定参数,如果没有,则需要使用适当的引用例程。

兔子洞很深。

顺便说一句,有关 Stack Overflow 上的问题很明显,请参阅https://stackoverflow.com/questions/37878426/conditional-where-expression-in-dynamic-query/37878574#37878574

在https://stackoverflow.com/questions/38016764/perform-create-index-in-plpgsql-doesnt-run/38021245#38021245上还有一个更成问题的版本(由于实用程序语句的限制)