带有 SQL 注入保护的简单查询比没有的要花费更长的时间

Posted

技术标签:

【中文标题】带有 SQL 注入保护的简单查询比没有的要花费更长的时间【英文标题】:Simple Query with SQL injection protection takes magnitudes longer than without 【发布时间】:2021-07-15 18:52:06 【问题描述】:

我对 Oracle 非常缺乏经验。这是怎么回事?

查询 A:

SELECT COUNT(*) 
  FROM MUHSCHEMA.MUH_TABLE
 WHERE MUH_DATE = TO_DATE(
                          TRIM(
                            '''' FROM SYS.DBMS_ASSERT.ENQUOTE_LITERAL('09/30/2020')), 
                            'mm/dd/yyyy'
                          );

查询 B:

SELECT COUNT(*) 
  FROM MUHSCHEMA.MUH_TABLE
 WHERE MUH_DATE = TO_DATE('09/30/2020', 'mm/dd/yyyy');

查询 A 大约需要 22 分钟。查询 B 大约需要 28 秒。而且,看起来,带有或不带有 ENQUOTE_LITERALTO_DATE 调用都返回相同的内容。

为什么查询 A 需要这么长时间?

查询计划:

| Id  | Operation                 | Name                     | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
----------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT          |                          |     1 |     9 |   411K  (2)| 00:00:17 |       |       |
|   1 |  SORT AGGREGATE           |                          |     1 |     9 |            |          |       |       |
|   2 |   VIEW                    | A_TABLE                  |    71M|   610M|   411K  (2)| 00:00:17 |       |       |
|   3 |    UNION-ALL              |                          |       |       |            |          |       |       |
|   4 |     PARTITION RANGE ALL   |                          |    28M|   214M| 42669  (15)| 00:00:02 |     1 |1048575|
|   5 |      PARTITION LIST ALL   |                          |    28M|   214M| 42669  (15)| 00:00:02 |     1 |    25 |
|*  6 |       INDEX FAST FULL SCAN| A_TABLE.                 |    28M|   214M| 42669  (15)| 00:00:02 |     1 |1048575|
|   7 |     PARTITION RANGE ALL   |                          |    42M|   327M|   368K  (1)| 00:00:15 |     1 |1048575|
|   8 |      PARTITION LIST ALL   |                          |    42M|   327M|   368K  (1)| 00:00:15 |     1 |    25 |
|*  9 |       INDEX RANGE SCAN    | A_TABLE.                 |    42M|   327M|   368K  (1)| 00:00:15 |     1 |1048575|
----------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

"   6 - filter(""MUH_DATE""=TO_DATE(TRIM('''' FROM ""DBMS_ASSERT"".""ENQUOTE_LITERAL""('09/30/2020')),'mm/dd/yy"
              yy'))
"   9 - access(""MUH_DATE""=TO_DATE(TRIM('''' FROM ""DBMS_ASSERT"".""ENQUOTE_LITERAL""('09/30/2020')),'mm/dd/yy"
              yy'))

查询 B 计划:

----------------------------------------------------------------------------------------------------------------------
| Id  | Operation                 | Name                     | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
----------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT          |                          |     1 |     9 | 36612   (1)| 00:00:02 |       |       |
|   1 |  SORT AGGREGATE           |                          |     1 |     9 |            |          |       |       |
|   2 |   VIEW                    | A_TABLE.                 |    28M|   241M| 36612   (1)| 00:00:02 |       |       |
|   3 |    UNION-ALL              |                          |       |       |            |          |       |       |
|   4 |     PARTITION RANGE SINGLE|                          |    28M|   214M| 36608   (1)| 00:00:02 |   250 |   250 |
|   5 |      PARTITION LIST ALL   |                          |    28M|   214M| 36608   (1)| 00:00:02 |     1 |    25 |
|*  6 |       INDEX FAST FULL SCAN| A_TABLE                  |    28M|   214M| 36608   (1)| 00:00:02 |  6226 |  6250 |
|   7 |     PARTITION RANGE SINGLE|                          |     1 |     8 |     4   (0)| 00:00:01 |    93 |    93 |
|   8 |      PARTITION LIST ALL   |                          |     1 |     8 |     4   (0)| 00:00:01 |     1 |    25 |
|*  9 |       INDEX RANGE SCAN    | A_TABLE.                 |     1 |     8 |     4   (0)| 00:00:01 |  2301 |  2325 |
----------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

"   6 - filter(""MUH_DATE""=TO_DATE(' 2020-09-30 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))"
"   9 - access(""MUH_DATE""=TO_DATE(' 2020-09-30 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))"

【问题讨论】:

顺便说一句,您可能已经意识到,但这并不是保护此类查询免受 SQL 注入影响的最佳方法 - 您通常会改用绑定变量,这有额外的好处(减少对查询的解析,不会用类似的查询淹没 SGA,等等) 它看起来很像enquote_literal,并且正在为表中的每一行重复转换为日期;我可以在具有未索引日期列的大型(非分区)表上复制效果 - 添加索引似乎可以治愈它,并且也会加快 28 年代版本的速度。或者您可以在物化 CTE 中转换一次值,但这似乎有点混乱。正如 Boneist 所说,如果可能的话,最好传入一个已经是日期的绑定值。 我不明白的部分是为什么您希望通过查询中的硬编码文字来实现 SQL 注入。实际上,您是否在那个地方使用了替换变量?如果是,Oracle 无法知道运行时的值在每一行中都相同(SQL 不理解 SQL*Plus 脚本语言,即使它理解了,您的查询也应该使用 && 表示法以表明该变量在任何地方都获得相同的文字值)。我要说的是:如果那是您的确切查询,那么您为什么要那样做?如果不同,请向我们展示真实的。 @steamrolla - 我也是……我的猜测是,如果没有索引,它总是会进行全表扫描,而它会提前评估日期以确定是否可以进行索引全扫描或范围扫描。在您的示例中,分区修剪可能会发生类似的事情。不确定这是否真的有意义;除了现在该计划有一个访问谓词,而不是过滤谓词 - 所以可能对这些进行不同的评估。 (虽然听起来仍然有点像一个错误,所以可能值得向 Oracle 询问。) 似乎在“partition range all”和“partition range single”处发生了变化。这表明 trim..enquote 会阻止 QP 理解/应用修剪 — docs.oracle.com/database/121/VLDBG/… 【参考方案1】:

值 '09/30/2020' 来自网络请求

那么任何处理 Web 请求的东西几乎肯定会支持参数化查询和绑定变量。不要尝试使用字符串连接构建查询,然后使用DBMS_ASSERT 来尝试防止 SQL 注入,只需使用绑定变量即可。

匿名绑定变量通常具有 ? 占位符(但您应该检查处理 Web 请求的任何服务的语法):

SELECT COUNT(*) 
FROM   MUHSCHEMA.MUH_TABLE
WHERE  MUH_DATE = TO_DATE( ?, 'mm/dd/yyyy');

或者命名的绑定变量通常以:为前缀,像这样:

SELECT COUNT(*) 
FROM   MUHSCHEMA.MUH_TABLE
WHERE  MUH_DATE = TO_DATE( :variable_name, 'mm/dd/yyyy');

更好的是,如果您可以在处理 Web 请求的任何内容中将字符串转换为日期,那么您可以将日期值传递给绑定变量,而无需使用 TO_DATE

SELECT COUNT(*) 
FROM   MUHSCHEMA.MUH_TABLE
WHERE  MUH_DATE = :date_variable_name;

我们正在讨论 oracle 如何将看似两个相似的查询转换为两个完全不同的实现。

如果一个人正在执行全表扫描并尝试在每一行上使用DBMS_ASSERT.ENQUOTE_LITERAL,那么重复执行此操作将花费大量时间。解决方案可能是使用索引,但更好的解决方案是根本不使用DBMS_ASSERT.ENQUOTE_LITERAL,而是通过绑定变量将值作为DATE 数据类型传递给查询。

您在 cmets 中引用的 How to write SQL injection proof PL/SQL 文档在第 31 页的状态:

规则 6:除非你不能,否则使用编译时固定的 SQL 语句文本。

绑定变量让您可以使用编译时固定的 SQL 语句;无论变量的值是什么,语句都不需要更改,您可以确定查询不会受到 SQL 注入的影响。

【讨论】:

有趣的是,我已经使用github.com/oracle/node-oracledb 绑定变量...并且获得了与 DBMS_ASSERT 查询相同的性能。此外,添加索引并不能回答为什么 oracle 想要重新评估我认为会在任何表扫描开始之前评估一次的问题......而且,我无法控制数据库及其维护方式:(

以上是关于带有 SQL 注入保护的简单查询比没有的要花费更长的时间的主要内容,如果未能解决你的问题,请参考以下文章

这个查询是不是受到 sql 注入的保护? [复制]

带有“where exists”的“Set”比没有的效果更好

如何保护带有参数化表名的查询不被注入

Spring DAO - 使用 JdbcTemplate 的第一次查询需要更长的时间

NHibernate 执行简单查询需要更长的时间

没有附加列的子查询比有列的子查询花费更长的时间