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

2022-09-03 14:45:57

我有这样的代码。

   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;
 }

这是环境的名称(可以随时间更改)和表名称(将被修复)。schemaDBtable

的值来自使查询容易受到 SQL 注入攻击的文件。schemaDBXML

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

任何人都可以建议我,可能采取什么方法来解决这个问题?

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


答案 1

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

所以你无法写作,或实现这种功能:

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

并且已经绑定到语句的表名中。TUSER

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

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

模式 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= ?");

如果需要,这种创建本身可以通过声明来动态化。 将返回 SQL 用户有权访问的所有表,您还可以从中获取表名和架构名。select * from ALL_TABLES;ALL_TABLES

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。为了提高安全性,标准应用程序用户不应写入此表。

不知何故,我觉得第一种模式更好。


答案 2

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

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库。


推荐