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

Posted

技术标签:

【中文标题】从搜索表单动态构建 WHERE 子句时如何防止 SQL 注入?【英文标题】:How to protect against SQL injection when the WHERE clause is built dynamically from search form? 【发布时间】:2010-11-12 13:12:09 【问题描述】:

我知道在 Java 中保护 SQL 查询免受 SQL 注入的唯一真正正确的方法是使用 PreparedStatements。

但是,这样的语句要求基本结构(选择的属性、连接的表、WHERE 条件的结构)不会发生变化。

我这里有一个 JSP 应用程序,它包含一个包含十几个字段的搜索表单。但用户不必填写所有这些 - 只需填写他需要的一个。因此我的 WHERE 条件每次都不一样。

我应该怎么做才能防止 SQL 注入? 转义用户提供的值?编写一个包装类,每次都构建一个 PreparedStatement?还是别的什么?

数据库是 PostgreSQL 8.4,但我更喜欢通用的解决方案。

非常感谢。

【问题讨论】:

此问题必须与以下至少一项重复:***.com/questions/485023/… | ***.com/questions/1812891/… | ***.com/questions/350177/… | ***.com/questions/1115739/… 我用的是这个:***.com/questions/1812891/… 答案基本上是:使用PreparedStatement 在发布这个问题之前,我阅读了大部分这些内容,但似乎没有一个涉及动态 SQL 查询。 @T.J. Crowder:他们有一个静态的 INSERT。我有一个动态选择。我已经使用 PreparedStatements 进行静态查询。 无论您处理的是insert 还是select,原理都是一样的:对于所有用户提供的值,使用PreparedStatement。您的问题与其他问题并没有真正的不同,只是您需要为语句(以及单独参数)构建 SQL,但基本是相同的。 【参考方案1】:

你见过 JDBC NamedParameterJDBCTemplate 吗?

NamedParameterJdbcTemplate 类 添加对 JDBC 编程的支持 使用命名参数的语句(如 反对编写 JDBC 语句 仅使用经典占位符 ('?') 论据。

您可以执行以下操作:

String sql = "select count(0) from T_ACTOR where first_name = :first_name";
SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);
return namedParameterJdbcTemplate.queryForInt(sql, namedParameters);

并动态构建您的查询字符串,然后类似地构建您的SqlParameterSource

【讨论】:

谢谢布赖恩,这看起来很有趣。我肯定会看看它的依赖关系,因为我宁愿避免将整个 Spring 库添加到我们的应用程序中。 我认为 Spring JDBC 应该是相当独立的。【参考方案2】:

我认为从根本上说,这个问题 与我在上面的评论中提到的其他问题相同,但我明白你为什么不同意 - 你正在更改 @987654321 中的内容@ 子句基于用户提供的内容。

不过,这仍然与在 SQL 查询中使用用户提供的数据不同,您肯定希望使用 PreparedStatement。它实际上非常类似于需要使用带有PreparedStatementin 语句的标准问题(例如,where fieldName in (?, ?, ?),但您事先不知道您需要多少?)。您只需要根据用户提供的信息动态构建查询并动态添加参数(但直接在查询中包含该信息)。

这是我的意思的一个例子:

// You'd have just the one instance of this map somewhere:
Map<String,String> fieldNameToColumnName = new HashMap<String,String>();
// You'd actually load these from configuration somewhere rather than hard-coding them
fieldNameToColumnName.put("title", "TITLE");
fieldNameToColumnName.put("firstname", "FNAME");
fieldNameToColumnName.put("lastname", "LNAME");
// ...etc.

// Then in a class somewhere that's used by the JSP, have the code that
// processes requests from users:
public AppropriateResultBean[] doSearch(Map<String,String> parameters)
throws SQLException, IllegalArgumentException

    StringBuilder           sql;
    String                  columnName;
    List<String>            paramValues;
    AppropriateResultBean[] rv;

    // Start the SQL statement; again you'd probably load the prefix SQL
    // from configuration somewhere rather than hard-coding it here.
    sql = new StringBuilder(2000);
    sql.append("select appropriate,fields from mytable where ");

    // Loop through the given parameters.
    // This loop assumes you don't need to preserve some sort of order
    // in the params, but is easily adjusted if you do.
    paramValues = new ArrayList<String>(parameters.size());
    for (Map.Entry<String,String> entry : parameters.entrySet())
    
        // Only process fields that aren't blank.
        if (entry.getValue().length() > 0)
        
            // Get the DB column name that corresponds to this form
            // field name.
            columnName = fieldNameToColumnName.get(entry.getKey());
                      // ^-- You'll probably need to prefix this with something, it's not likely to be part of this instance
            if (columnName == null)
            
                // Somehow, the user got an unknown field into the request
                // and that got past the code calling us (perhaps the code
                // calling us just used `request.getParameterMap` directly).
                // We don't allow unknown fields.
                throw new IllegalArgumentException(/* ... */);
            
            if (paramValues.size() > 0)
            
                sql.append("and ");
            
            sql.append(columnName);
            sql.append(" = ? ");
            paramValues.add(entry.getValue());
        
    

    // I'll assume no parameters is an invalid case, but you can adjust the
    // below if that's not correct.
    if (paramValues.size() == 0)
    
        // My read of the problem being solved suggests this is not an
        // exceptional condition (users frequently forget to fill things
        // in), and so I'd use a flag value (null) for this case. But you
        // might go with an exception (you'd know best), either way.
        rv = null;
    
    else
    
        // Do the DB work (below)
        rv = this.buildBeansFor(sql.toString(), paramValues);
    

    // Done
    return rv;


private AppropriateResultBean[] buildBeansFor(
    String sql,
    List<String> paramValues
)
throws SQLException

    PreparedStatement       ps      = null;
    Connection              con     = null;
    int                     index;
    AppropriateResultBean[] rv;

    assert sql != null && sql.length() > 0);
    assert paramValues != null && paramValues.size() > 0;

    try
    
        // Get a connection
        con = /* ...however you get connections, whether it's JNDI or some conn pool or ... */;

        // Prepare the statement
        ps = con.prepareStatement(sql);

        // Fill in the values
        index = 0;
        for (String value : paramValues)
        
            ps.setString(++index, value);
        

        // Execute the query
        rs = ps.executeQuery();

        /* ...loop through results, creating AppropriateResultBean instances
         * and filling in your array/list/whatever...
         */
        rv = /* ...convert the result to what we'll return */;

        // Close the DB resources (you probably have utility code for this)
        rs.close();
        rs = null;
        ps.close();
        ps = null;
        con.close(); // ...assuming pool overrides `close` and expects it to mean "release back to pool", most good pools do
        con = null;

        // Done
        return rv;
    
    finally
    
        /* If `rs`, `ps`, or `con` is !null, we're processing an exception.
         * Clean up the DB resources *without* allowing any exception to be
         * thrown, as we don't want to hide the original exception.
         */
    

请注意我们如何使用用户提供给我们的信息(他们填写的字段),但我们从来没有将他们实际提供的任何内容直接放入我们执行的 SQL 中,我们总是运行它PreparedStatement.

【讨论】:

它类似于in(?, ?, ?, ...),但有一个重要的区别可以用另一种方式解决 - 请参阅我刚刚添加的答案。【参考方案3】:

最好的解决方案是使用一个中间件来进行数据验证和绑定,并充当 JSP 和数据库之间的中介。

可能有一个列名列表,但它是有限且可数的。让 JSP 担心让中间层知道用户的选择;让中间层在发送到数据库之前绑定和验证。

【讨论】:

在我的特殊情况下,列名列表实际上是静态的。但我可以在 WHERE 子句中包含一到十五个条件。 它始终是静态的,除非您打算即时更改表。问题是组合的数量吗?即使有 15 列,组合的数量也会很快变大 用户可以请求多少个 ata 时间?【参考方案4】:

对于这种特殊情况,这是一种有用的技术,您的WHERE 中有许多子句,但您事先不知道需要应用哪些子句。

您的用户会按标题搜索吗?

select id, title, author from book where title = :title

还是作者?

select id, title, author from book where author = :author

或两者兼而有之?

select id, title, author from book where title = :title and author = :author

只有 2 个字段就够糟糕了。组合的数量(因此不同的 PreparedStatements)随着条件的数量呈指数增长。诚然,您的 PreparedStatement 池中可能有足够的空间容纳所有这些组合,并且要在 Java 中以编程方式构建子句,您只需要每个条件一个 if 分支。不过,它并不那么漂亮。

您可以通过简单地编写一个看起来相同的SELECT 来巧妙地解决此问题,无论是否需要每个单独的条件。

我几乎不需要提及您使用其他答案所建议的PreparedStatement,如果您使用的是 Spring,NamedParameterJdbcTemplate 很好。

这里是:

select id, title, author
from book
where coalesce(:title, title) = title
and coalesce(:author, author) = author

然后为每个未使用的条件提供NULLcoalesce() 是一个返回其第一个非空参数的函数。因此,如果您将NULL 传递给:title,则第一个子句是where coalesce(NULL, title) = title,其计算结果为where title = title,它始终为真,对结果没有影响。

根据优化器处理此类查询的方式,您可能会受到性能影响。但可能不在现代数据库中。

(虽然类似,但这个问题IN (?, ?, ?)子句问题相同,你不知道列表中的值的数量,因为这里你 em> 有固定数量的可能子句,您只需要单独激活/停用它们。)

【讨论】:

【参考方案5】:

我不确定是否存在在 php 的 PDO 中广泛使用的 quote() 方法。这将为您提供更灵活的查询构建方法。

此外,其中一种可能的想法是创建特殊类,该类将处理过滤条件并将所有占位符及其值保存到堆栈中。

【讨论】:

以上是关于从搜索表单动态构建 WHERE 子句时如何防止 SQL 注入?的主要内容,如果未能解决你的问题,请参考以下文章

动态 SELECT 和 WHERE 子句

Linq to Entities 中的动态 where 子句 (OR)

如何通过文本框过滤数据表视图中的子表单? #likeoperator #where 子句

如何强制 SQL Server 在 WHERE 子句之前处理 CONTAINS 子句?

SQLite 动态 WHERE 子句不起作用

以 oracle apex 形式创建动态 where 子句