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

时间:2010-11-12 13:12:09

标签: java sql postgresql jdbc

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

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

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

我该怎么做才能阻止SQL注入?
逃避用户提供的值?编写一个包装类,每次都构建一个PreparedStatement?或其他什么?

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

提前多多感谢。

5 个答案:

答案 0 :(得分:6)

你见过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

答案 1 :(得分:2)

我认为从根本上说,这个问题与我在上面的评论中提到的其他问题相同,但我确实看到你不同意的原因 - 你正在改变你{{{ 1}}子句基于用户提供的内容。

但这仍然与在SQL查询中使用用户提供的数据不同,您肯定希望使用where。它实际上非常类似于需要在PreparedStatement使用in语句的标准问题(例如,PreparedStatement,但您事先并不知道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

答案 2 :(得分:1)

最佳解决方案是使用中间数据验证和绑定,并充当JSP和数据库之间的中介。

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

答案 3 :(得分:0)

我不确定是否有一个quote()方法,它在PHP的PDO中被广泛使用。这将允许您更灵活的查询构建方法。

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

答案 4 :(得分:0)

对于这种特殊情况,这是一种有用的技巧,您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参数的函数。因此,如果您为NULL传递:title,则第一个子句为where coalesce(NULL, title) = title,其值为where title = title,始终为真,对结果没有影响。

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

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