当语句具有动态表名时,如何防止SQL注入?
我有这样的代码。
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兼容(如果可能的话,与数据库无关)。
遗憾的是,JDBC不允许您将表名作为语句中的绑定变量。 (这有其原因)。
所以你不能写,或实现这种function:
connection.prepareStatement("SELECT * FROM ? where id=?", "TUSERS", 123);
并且将TUSER
绑定到语句的表名。
因此,您唯一安全的方法是validation用户输入。 但最安全的方法不是validation它并允许用户输入通过数据库,因为从安全的角度来看,您总是可以指望用户比validation更聪明。 永远不要相信在您的语句中连接的动态的,用户生成的String。
那么什么是安全的validation模式?
模式1:预建安全查询
1)在代码中一劳永逸地创建所有有效语句。
Map 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;
将此创建本身设置为动态select * from ALL_TABLES;
声明。 ALL_TABLES
将返回SQL用户有权访问的所有表,您还可以从中获取表名和模式名称。
2)选择地图内的语句
String unsafeUserContent = ... String safeStatement = statementByTableName.get(usafeUserContent); conn.prepareStatement(safeStatement, name);
了解unsafeUserContent
变量如何永远不会到达DB。
3)制定某种策略或unit testing,检查所有statementByTableName
是否对您的模式有效,以便将来进行演变,并且不会丢失任何表。
模式2:仔细检查
你可以1)使用免注入查询validation用户输入确实是一个表名(我在这里输入伪sql代码,你必须调整它才能使它工作,因为我没有Oracle实例来实际检查有用) :
select * FROM (select schema_name || '.' || table_name as fullName FROM all_tables) WHERE fullName = ?
并在此处将您的fullName绑定为预准备语句变量。 如果您有结果,那么它是一个有效的表名。 然后,您可以使用此结果来构建安全查询。
模式3
它是1到2之间的混合。您创建一个名为“TABLES_ALLOWED_FOR_DELETION”的表,并使用适合删除的所有表静态填充它。
然后进行validation步骤
conn.prepareStatement(SELECT safe_table_name FROM TABLES_ALLOWED_FOR_DELETION WHERE table_name = ?", unsafeDynamicString);
如果有结果,则执行safe_table_name。 为了更加安全,标准应用程序用户不应该写入此表。
我不知何故觉得第一种模式更好。
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
您可以使用正则表达式检查表名来避免攻击:
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库。
我认为最好的方法是创建一组可能的表名,并在创建查询之前检查此集中是否存在。
Set 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! }