使用预处理语句设置表名

34

我正在尝试使用预处理语句来设置一个表名以选择数据,但是当我执行查询时,我不断收到错误提示。

下面显示了错误信息和示例代码。

[Microsoft][ODBC Microsoft Access Driver] Parameter 'Pa_RaM000' specified where a table name is required.



private String query1 = "SELECT plantID, edrman, plant, vaxnode FROM [?]"; //?=date
public Execute(String reportDate){
    try {

        Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
        Connection conn = DriverManager.getConnection(Display.DB_MERC);
        PreparedStatement st = conn.prepareStatement(query1);
        st.setString(1, reportDate);
        ResultSet rs = st.executeQuery();

有什么想法可能导致这种情况吗?


是的,输入过滤以防止 SQL 注入攻击! - i.am.michiel
2
如果您需要将不同的表名替换为具有相同结构的查询,则指向数据库设计中的缺陷。至少它指向具有相同关系属性的多个表格。将其规范化为一个带有“主题”列的单个表格。 - Terrible Tadpole
8个回答

38

表名不能用作参数,它必须是硬编码的。因此,您可以这样做:

private String query1 = "SELECT plantID, edrman, plant, vaxnode FROM [" + reportDate + "?]";

20
不要这样做!这是一个安全问题,可能会引起SQL注入。 - Benvorth
8
在表名上使用org.apache.commons.lang.StringEscapeUtils.escapeSql函数,以确保您不会危及数据库架构。 - Sebas
6
@everyone 建议不要将表名进行字符串连接操作,这样会导致 SQL 注入攻击,而 SQL 注入攻击是 OWASP 十大安全威胁中排名第一的攻击方式(详见 https://www.owasp.org/index.php/Top_10_2013-Top_10)。 - Benvorth
7
毋庸置疑,如果您要这样做,那么表名必须列入白名单(而且您应该使用来自白名单的值而不是输入的值,以确保安全;可以使用正则表达式等方法)。 - Richard Tingle
13
不要使用 org.apache.commons.lang.StringEscapeUtils.escapeSql。它已经在 commons.lang3 中被弃用,并且无论如何只会将单引号替换为双单引号。它不能防止 SQL 注入。请参见 https://commons.apache.org/proper/commons-lang/article3_0.html 和 http://commons.apache.org/proper/commons-lang/javadocs/api-2.6/org/apache/commons/lang/StringEscapeUtils.html#escapeSql%28java.lang.String%29。 - Philip Callender
显示剩余2条评论

3

如果您需要一个不易受SQL注入攻击的解决方案,您需要为所需的所有表格复制查询:

最初的回答:

如果你需要一个不容易遭受SQL注入攻击的解决方案,你必须为所有需要的表格复制查询。

final static String QUERIES = {
    "SELECT x FROM Table1 x WHERE a=:a AND b=:b AND ...",
    "SELECT x FROM Table2 x WHERE a=:a AND b=:b AND ...",
    "SELECT x FROM Table3 x WHERE a=:a AND b=:b AND ...",
    ...
};

是的,这些查询是重复的,只有表名不同。

现在你只需要选择适合你的表格的查询,例如像

最初的回答: 是的,这些查询是重复的,只有表名不同。

现在你只需要选择适合你的表格的查询,例如像

...
PreparedStatement st = conn.prepareStatement(QUERIES[index]);
...

你可以使用JPA、Hibernate或其他技术来实现这一点...如果你想要更详细的方法,考虑使用枚举类型,比如Original Answer。
enum AQuery {
    Table1("SELECT x FROM Table1 x WHERE a=:a AND b=:b AND ..."),
    Table2("SELECT x FROM Table2 x WHERE a=:a AND b=:b AND ..."),
    Table3("SELECT x FROM Table3 x WHERE a=:a AND b=:b AND ..."),
    ...

    private final String query;
    AQuery(final String query) {
        this.query = query;
    }

    public String getQuery() {
        return query;
    }
}

现在可以使用索引来定位。原始答案翻译成“最初的回答”。
String sql = AQuery.values()[index].getQuery();
PreparedStatement st = conn.prepareStatement(sql);
...

Or use a table name

String sql = AQuery.valueOf("Table1").getQuery();
PreparedStatement st = conn.prepareStatement(sql);
...

2

无法在预处理语句中设置表名

如前所述,使用 preparedStatement.setString(1, tableName) 无法在预处理语句中设置表名。也无法将 SQL 查询的部分添加到预处理语句中(例如 preparedStatement.addSql(" or xyz is null"))。

如何正确操作而不冒 SQL 注入风险?

必须使用字符串操作将表名插入要执行的 SQL(或 JQL)查询中,例如 "select * from " + tableNameString.format("select * from %s", tableName)

但如何避免 SQL 注入?

如果表名不来自用户输入,则可能是安全的。例如,如果您像这里一样做出决定:

String tableName;
if(condition) {
    tableName = "animal";
} else {
    tableName = "plant";
}
final String sqlQuery = "delete from " + tableName;
...


如果表名取决于用户输入,则需要手动检查输入。 例如,使用包含所有有效表名的白名单
if(!tableNamesWhitelist.contains(tableName)) {
    throw new IllegalArgumentException(tableName + " is not a valid table name");
}
String sqlQuery = "delete from " + tableName;

或者使用 枚举

public enum Table {
    ANIMAL("animal"),
    PLANT("plant");

    private sqlTableName;
    private TableName(String sqlTableName) {
        this.sqlTableName= sqlTableName;
    }
    public getSqlTableName() {
        return sqlTableName;
    }
}

然后将用户输入的字符串如ANIMAL转换为Table.ANIMAL。如果没有匹配的枚举值存在,则会抛出异常。

例如:

@DeleteMapping("/{table}")
public String deleteByEnum(@PathVariable("table") Table table) {
    final String sqlQuery = "delete from " + table.getSqlTableName();
    ...
}


当然,这些示例也适用于select、update等许多其他实现,检查用户输入的方法也有很多种。

1
这在技术上是可能的,但是采用绕过方式实现会带来很糟糕的后果。
String sql = "IF ? = 99\n";
sql += "SELECT * FROM first_table\n";
sql += "ELSE\n";
sql += "SELECT * FROM second_table";
PreparedStatement ps = con.prepareStatement(sql);

然后,当你想从 first_table 中进行选择时,需要设置以下参数:

ps.setInt(1, 99);

如果不是这样,你可以将其设置为其他内容。

0

正如许多人所说,您不能将语句参数用作表名,只能将其用作条件的变量。

基于您具有(至少)两个表名的可变表名的事实,也许最好创建一个方法,该方法接受您正在存储的实体并返回一个准备好的语句。

PreparedStatement p = createStatement(table);

-1
这可能会有所帮助:
public ResultSet getSomething(String tableName) {

PreparedStatement ps = conn.prepareStatement("select * from \`"+tableName+"\`");
ResultSet rs = ps.executeQuery();
}

-3

我不确定您是否可以使用PreparedStatement指定表的名称,只能指定某些字段的值。无论如何,您可以尝试相同的查询,但是不要使用括号:

"SELECT plantID, edrman, plant, vaxnode FROM ?"

你需要使用括号来转义查询中的“/”...或者这也可能是针对日期的。我去年夏天查过。 - Brandon

-3
String table="pass"; 

String st="select * from " + table + " ";

PreparedStatement ps=con.prepareStatement(st);

ResultSet rs = ps.executeQuery();

1
你能提供一些上下文来解释你的答案吗?这样未来的读者就可以学习如何将其应用到他们自己的问题中,而不仅仅是在这种情况下。 - Newd
6
易受 SQL 注入攻击。不要这样做! - Terrible Tadpole

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接