语句有动态表名时如何防止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 Server 中清理(防止 SQL 注入)动态 SQL?