未经许可,不得转载。

从 JDBC 到 预编译
在早期的 Java 数据库开发中,SQL 语句通常采用字符串拼接的方式构造,例如:
public User getUserById(String id) throws SQLException {
Connection connection = JDBCTOOLS.getConnection();
String sql = "SELECT id, username FROM user WHERE id=" + id;
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
resultSet.next();
int userId = resultSet.getInt(1);
String username = resultSet.getString(2);
User user = new User(userId, username);
return user;
}
在上述代码中,SQL 语句中直接拼接了 id 变量,而该变量是用户输入的内容,极易引发 SQL 注入 攻击。攻击者可以构造1 OR 1=1,从而绕过身份验证,甚至窃取或修改数据库中的数据。
预编译机制的引入
为了解决 SQL 注入问题,JDBC 引入了预编译(PreparedStatement)机制,它的核心思想是将 SQL 语句与用户输入分离,避免外部输入影响 SQL 结构。例如,正确的查询用户数据方式如下:
public User getUserById(String id) throws SQLException {
Connection connection = JDBCTOOLS.getConnection();
String sql = "SELECT id, username FROM user WHERE id = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, id);
ResultSet resultSet = preparedStatement.executeQuery();
resultSet.next();
int userId = resultSet.getInt(1);
String username = resultSet.getString(2);
return new User(userId, username);
}
在这里,? 作为 占位符,用户输入的 id 会作为参数传递给数据库,而不是直接拼接到 SQL 语句中。这样可以避免 SQL 注入,并提升查询性能(数据库可以对预编译语句进行缓存和优化)。
预编译的局限性
尽管预编译机制极大地提升了 SQL 语句的安全性,但它只能用于查询参数,而不能用于 SQL 结构,这在某些场景下会带来限制。
1. LIKE 语句模糊查询
在模糊查询中,开发者可能会尝试如下写法:
String sql = "SELECT * FROM user WHERE username LIKE '%?%'";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, keyword);
ResultSet resultSet = preparedStatement.executeQuery();
然而,这种写法会 导致 SQL 语法错误,因为数据库会将 ? 视为一个完整的字符串,而不是 SQL 语句的一部分。实际执行的 SQL 可能变成:
SELECT * FROM user WHERE username LIKE '%?%'
这显然不是开发者想要的结果。
正确的做法 是在 Java 代码中手动拼接 %,然后再绑定参数:
String sql = "SELECT * FROM user WHERE username LIKE ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, "%" + keyword + "%");
ResultSet resultSet = preparedStatement.executeQuery();
这种方式避免了直接拼接 SQL 语句,同时仍然保持一定程度的安全性。
另一种错误的代码如下:
<select id="findlike" resultType="com.test.domain.User" parameterType="string">
SELECT * FROM user WHERE name LIKE '%${name}%'
</select>
${name} 会直接将用户输入的值拼接到 SQL 语句中,容易造成 SQL 注入漏洞。
正确做法:使用 #{} 绑定参数,并通过 CONCAT 拼接 %,保证 SQL 语句的安全性:
<select id="findlike" resultType="com.test.domain.User" parameterType="string">
SELECT * FROM user WHERE name LIKE CONCAT('%', #{name}, '%')
</select>
需要拼接的原因是:SELECT * FROM user WHERE name LIKE ‘%#{name}%’ 不能防止SQL注入。
因为 #{name} 是 MyBatis 的占位符,不能直接拼接到 SQL 语句的字符串中,否则会导致 SQL 语法错误。
2. ORDER BY 动态排序
在需要 动态排序 时(如按用户名、时间等字段排序),开发者可能会这样写:
String sort = req.getParameter("sort");
String sql = "SELECT * FROM user ORDER BY ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, sort);
ResultSet resultSet = preparedStatement.executeQuery();
问题:数据库会将字段名解析为字符串,最终执行的 SQL 实际上变成:
SELECT * FROM user ORDER BY 'username';
由于 'username' 是字符串而非字段名,SQL 语句不会按照预期执行。
正确的做法是使用白名单机制,确保只能按特定字段排序:
String sort = req.getParameter("sort");
List<String> allowedSortFields = Arrays.asList("id", "username", "created_at");
// 检查字段是否合法,防止 SQL 注入
if (!allowedSortFields.contains(sort)) {
throw new IllegalArgumentException("非法排序字段");
}
String sql = "SELECT * FROM user ORDER BY " + sort;
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery();
这种方式虽然需要拼接 SQL,但由于 sort 只能从受控的字段列表中选择,因此不会引发 SQL 注入问题。
3.in后注入
错误代码示例:
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
SELECT * FROM user_table WHERE username IN (${usernames})
</select>
${usernames} 直接拼接字符串,会导致 SQL 注入。
推荐使用 MyBatis foreach 标签 动态构建 IN 语句:
<select id="getUserFromList" resultType="user.NewUserDO">
SELECT * FROM user_table WHERE username IN
<foreach collection="list" item="username" open="(" separator="," close=")">
#{username}
</foreach>
</select>
综上所述,在对 Java 应用程序进行代码审计时,如果涉及 JDBC 连接操作,可以通过全局搜索以下关键字来定位潜在的 SQL 查询执行点:
statement
executeQuery
824

被折叠的 条评论
为什么被折叠?



