语句有动态表名时如何防止SQL注入?

Posted

技术标签:

【中文标题】语句有动态表名时如何防止SQL注入?【英文标题】:How to prevent SQL injection when the statement has a dynamic table name? 【发布时间】:2018-08-16 17:07:42 【问题描述】:

我有类似这样的代码。

   final PreparedStatement stmt = connection
                .prepareStatement("delete from " + fullTableName
                    + " where name= ?");
   stmt.setString(1, addressName);

fullTableName 的计算类似于:

 public String getFullTableName(final String table) 
    if (this.schemaDB != null) 
        return this.schemaDB + "." + table;
    
    return table;
 

这里schemaDB 是环境名称(可以随时间更改),table 是表名称(将被修复)。

schemaDB 的值来自XML 文件,这使得查询容易受到 SQL 注入的攻击。

查询:我不确定如何将表名用作准备好的语句(如本例中使用的name),这是针对 SQL 注入的 100% 安全措施。

谁能给我建议,有什么可能的方法来处理这个问题?

注意:我们将来可以迁移到 DB2,因此该解决方案应该与 Oracle 和 DB2 兼容(并且如果可能的话独立于数据库)。

【问题讨论】:

如果您控制 xml 文件(即,它不是用户可以更改/提供的),您应该没问题。 您不能在 PreparedStatement 中绑定表名,您必须接受 XML 文件是有效的(您可能会验证表名)或硬编码所有有效表并以这种方式解决。 @ElliottFrisch,是的,这就是我最后打算做的事情(在将表名附加到查询之前验证表名)。但我不能 100% 确定这是否是防止 SQL 注入的最佳方法。 让它更安全:为所有表预先构建完整语句列表(“DELETE FROM table_1 WHERE name= ?”、“DELETE FROM table_2 WHERE name = ?”等。放它们在(哈希)映射中。并且在连接字符串之前不验证表名是否正确。相反,根据用户生成的条目选择预先构建的、本质上安全的语句之一。这样,用户生成的任何内容都不会得到连接到您的语句。只有开发人员、预构建的查询才会访问数据库。 @GPI,通过验证表名,我的意思是检查字符串是否仅包含字母数字字符(因为我项目中的表名仅包含字母数字字符)。在这里我不能确定确切的表名。 【参考方案1】:

不幸的是,JDBC 不允许您将表名作为语句中的绑定变量。 (这是有原因的)。

所以你不能写,或者实现这种功能:

connection.prepareStatement("SELECT * FROM ? where id=?", "TUSERS", 123);

并让TUSER 绑定到语句的表名。

因此,您唯一安全的方法是验证用户输入。不过,最安全的方法不是验证它并允许用户输入通过数据库,因为从安全的角度来看,您总是可以指望用户比您的验证更聪明。 永远不要相信动态的、用户生成的、连接在语句中的字符串。

那么什么是安全验证模式?

模式 1:预构建安全查询

1) 在代码中一劳永逸地创建所有有效语句。

Map<String, String> statementByTableName = new HashMap<>();
statementByTableName.put("table_1", "DELETE FROM table_1 where name= ?");
statementByTableName.put("table_2", "DELETE FROM table_2 where name= ?");

如果需要,可以使用select * from ALL_TABLES; 语句使创建本身动态化。 ALL_TABLES 将返回您的 SQL 用户可以访问的所有表,您还可以从中获取表名和架构名称。

2) 选择地图内的语句

String unsafeUserContent = ...
String safeStatement = statementByTableName.get(usafeUserContent);
conn.prepareStatement(safeStatement, name);

看看unsafeUserContent 变量如何永远不会到达数据库。

3) 制定某种策略或单元测试,以检查您的所有 statementByTableName 是否对您的架构有效,以供将来发展,并且没有丢失任何表。

模式 2:双重检查

您可以 1) 验证用户输入确实是一个表名,使用无注入查询(我在这里输入伪 sql 代码,您必须对其进行调整以使其工作,因为我没有 Oracle 实例实际检查它是否有效):

select * FROM 
    (select schema_name || '.' || table_name as fullName FROM all_tables)
WHERE fullName = ?

并在此处将您的 fullName 绑定为准备好的语句变量。如果您有结果,那么它是一个有效的表名。然后你可以使用这个结果来构建一个安全的查询。

模式 3

这是 1 和 2 的混合体。 您创建一个名为“TABLES_ALLOWED_FOR_DELETION”的表,然后用所有适合删除的表静态填充它。

然后您将验证步骤变为

conn.prepareStatement(SELECT safe_table_name FROM TABLES_ALLOWED_FOR_DELETION WHERE table_name = ?", unsafeDynamicString);

如果这有结果,则执行 safe_table_name。为了更加安全,标准应用程序用户不应写入此表。

我觉得第一个模式更好。

【讨论】:

这是一个很好的解释,解决了我的问题,但突然知道我们可以在一段时间后迁移到 DB2,所以代码应该兼容数据库 DB2 和 Oracle(它如果我们可以使其独立于数据库,效率会更高)。如果您可以添加任何进一步的评论,它将帮助我找到解决方案。 第一个模式没有任何数据库依赖。第二个是,因为all_tables 是Oracle 特定的。我认为您不会找到可移植的“pattern2”实现。我将添加第三个变体。 要获取用户创建的表,需要schemaDB 名称,该名称来自XML 文件。我不确定我们是否可以使用通用查询来代替 ("CREATOR || '.' || name as fullTableName FROM SYSIBM.SYSTABLES" for DB2 和 "select owner || '.' || table_name as fullTableName FROM all_tables; " 代表甲骨文)。【参考方案2】:

我认为最好的方法是创建一组可能的表名,并在创建查询之前检查该组中是否存在。

Set<String> validTables=.... // prepare this set yourself

    if(validTables.contains(fullTableName))
    
       final PreparedStatement stmt = connection
                    .prepareStatement("delete from " + fullTableName
                        + " where name= ?");

    //and so on
    else
       // ooooh you nasty haker!
    

【讨论】:

【参考方案3】:

您可以通过使用正则表达式检查您的表名来避免攻击:

if (fullTableName.matches("[_a-zA-Z0-9\\.]+")) 
    final PreparedStatement stmt = connection
                .prepareStatement("delete from " + fullTableName
                    + " where name= ?");
    stmt.setString(1, addressName);

使用这样一组受限制的字符来注入 SQL 是不可能的。

此外,我们可以转义表名中的任何引号,并将其安全地添加到我们的查询中:

fullTableName = StringEscapeUtils.escapeSql(fullTableName);
final PreparedStatement stmt = connection
            .prepareStatement("delete from " + fullTableName
                + " where name= ?");
stmt.setString(1, addressName);

StringEscapeUtils 带有 Apache 的 commons-lang 库。

【讨论】:

是的。但是,至少在 Oracle 以外的一些数据库上,是否不可能有带变音符号的表名?美元符号或破折号什么的?西里尔文/中文/日文/什么名字?最诚实的问题,这会改变这个答案的有效性背景。 @GPI,这是大多数通用情况的答案,其想法是限制字符。【参考方案4】:
create table MYTAB(n number);
insert into MYTAB values(10);
commit;
select * from mytab;

N
10

create table TABS2DEL(tname varchar2(32));
insert into TABS2DEL values('MYTAB');
commit;
select * from TABS2DEL;

TNAME
MYTAB

create or replace procedure deltab(v in varchar2)
is

    LvSQL varchar2(32767);
    LvChk number;

begin
    LvChk := 0;
    begin
        select count(1)
          into LvChk
          from TABS2DEL
         where tname = v;

         if LvChk = 0 then
             raise_application_error(-20001, 'Input table name '||v||' is not a valid table name');
         end if;


    exception when others
              then raise;
    end;

    LvSQL := 'delete from '||v||' where n = 10';
    execute immediate LvSQL;
    commit;

end deltab;

begin
deltab('MYTAB');
end;

select * from mytab;

没有找到行

begin
deltab('InvalidTableName');
end;

ORA-20001: Input table name InvalidTableName is not a valid table name ORA-06512: at "SQL_PHOYNSAMOMWLFRCCFWUMTBQWC.DELTAB", line 21
ORA-06512: at "SQL_PHOYNSAMOMWLFRCCFWUMTBQWC.DELTAB", line 16
ORA-06512: at line 2
ORA-06512: at "SYS.DBMS_SQL", line 1721

【讨论】:

如果我正确理解你的答案,TAB2DEL 不是更好地命名为 "TABLES_ALLOWED_TO_BE_DELETED" 吗?是静态表吧?如果没有,我真的看不出你从应用程序代码中调用你的过程的方式是什么......

以上是关于语句有动态表名时如何防止SQL注入?的主要内容,如果未能解决你的问题,请参考以下文章

如何防止使用动态表名进行 SQL 注入?

怎样防止sql注入

如何在 SQL Server 中清理(防止 SQL 注入)动态 SQL?

mybatis在传参时,为啥#能够有效的防止sql注入

从搜索表单动态构建 WHERE 子句时如何防止 SQL 注入?

mysql中查询语句的表名,是不是可以动态选择表名像这样