【Java代码审计 | 第二篇】SQL注入之JDBC

未经许可,不得转载。

在这里插入图片描述

从 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋说

感谢打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值