从搜索表单动态构建 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
。它实际上非常类似于需要使用带有PreparedStatement
的in
语句的标准问题(例如,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
然后为每个未使用的条件提供NULL
。 coalesce()
是一个返回其第一个非空参数的函数。因此,如果您将NULL
传递给:title
,则第一个子句是where coalesce(NULL, title) = title
,其计算结果为where title = title
,它始终为真,对结果没有影响。
根据优化器处理此类查询的方式,您可能会受到性能影响。但可能不在现代数据库中。
(虽然类似,但这个问题不与IN (?, ?, ?)
子句问题相同,你不知道列表中的值的数量,因为这里你做 em> 有固定数量的可能子句,您只需要单独激活/停用它们。)
【讨论】:
【参考方案5】:我不确定是否存在在 php 的 PDO 中广泛使用的 quote() 方法。这将为您提供更灵活的查询构建方法。
此外,其中一种可能的想法是创建特殊类,该类将处理过滤条件并将所有占位符及其值保存到堆栈中。
【讨论】:
以上是关于从搜索表单动态构建 WHERE 子句时如何防止 SQL 注入?的主要内容,如果未能解决你的问题,请参考以下文章
Linq to Entities 中的动态 where 子句 (OR)
如何通过文本框过滤数据表视图中的子表单? #likeoperator #where 子句