我知道在Java中保护SQL查询以防止SQL注入的唯一正确方法是使用PreparedStatements。
但是,这样的声明要求基本结构(选定的属性,连接表,WHERE条件的结构)不会发生变化。
我这里有一个JSP应用程序,其中包含一个包含大约十几个字段的搜索表单。但是用户不必填写所有这些 - 只需要他需要的那个。因此,我的WHERE条件每次都不同。
我该怎么做才能阻止SQL注入?
逃避用户提供的值?编写一个包装类,每次都构建一个PreparedStatement?或其他什么?
数据库是PostgreSQL 8.4,但我更喜欢一般的解决方案。
提前多多感谢。
答案 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
然后为每个未使用的条件提供NULL
。 coalesce()
是一个返回其第一个非null参数的函数。因此,如果您为NULL
传递:title
,则第一个子句为where coalesce(NULL, title) = title
,其值为where title = title
,始终为真,对结果没有影响。
根据优化器处理此类查询的方式,您可能会受到性能影响。但可能不在现代数据库中。
(虽然类似,这个问题不与IN (?, ?, ?)
子句问题相同,你不知道列表中的值的数量,因为在这里你 具有固定数量的可能子句,您只需要单独激活/取消激活它们。)