【Java代码审计】SQL注入篇
1.SQL注入漏洞概述
SQL 注入(SQL Injection)是因为程序未能正确对用户的输入进行检查,将用户的输入以拼接的方式带入 SQL 语句中,导致了 SQL 注入的产生。黑客通过 SQL 注入可直接窃取数据库信息,造成信息泄露
造成 SQL 注入一般需要满足以下两个条件:
- 输入用户可控
- 直接或间接拼入 SQL语句执行
因 SQL 注入漏洞特征性较强,在实际的审计过程中我们往往可以通过一些自动化审计工具快速地发现这些可能存在安全问题的代码片,如使用奇安信代码卫士、Fortify 等自动化工具
Java 语言本身是一种强类型语言,因此在寻找 SQL 注入漏洞的过程中,可以首先找到所有包含 SQL 语句的点,随后观察传参类型是否是 String 类型,只有当传参类型是 String 类型时我们才可能进行 SQL 注入
2.JDBC Statement执行SQL语句导致SQL注入
java.sql.Statement是Java JDBC下执行SQL语句的一种原生方式,执行语句时需要通过拼接来执行。若拼接的语句没有经过过滤,将出现SQL注入漏洞
下面的例子中,我们直接将用户输入的用户名和密码拼接到SQL查询中,而没有使用参数化查询或预编译语句。这样的代码容易受到SQL注入攻击,因为恶意用户可以在输入中插入恶意SQL代码,从而修改SQL查询的行为:
@RestController
public class InsecureController {
@Autowired
private DataSource dataSource;
@GetMapping("/insecure-login")
public String insecureLogin(@RequestParam("username") String username, @RequestParam("password") String password) {
String sql = "SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'";
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
if (rs.next()) {
return "Login successful!";
} else {
return "Invalid username or password!";
}
} catch (SQLException e) {
e.printStackTrace();
return "An error occurred during login.";
}
}
}
假设用户输入了以下恶意输入:
用户名:admin' OR '1'='1
密码:任意密码
当这样的输入被拼接到SQL查询中时,查询语句会变成:
SELECT * FROM users WHERE username='admin' OR '1'='1' AND password='任意密码'
这个查询会返回所有用户信息
3.PreparedStatement执行SQL语句但依然采用拼接
JDBC 有两种方法执行 SQL 语句,分别为 PrepareStatement 和 Statement。两个方法的区别在于 PrepareStatement 会对 SQL 语句进行预编译,而 Statement 方法在每次执行时都需要编译,会增大系统开销。理论上 PrepareStatement 的效率和安全性会比 Statement 要好,但并不意味着使用 PrepareStatement 就绝对安全,不会产生 SQL注入
例如下面的例子:
@RestController
public class InsecureController {
@Autowired
private DataSource dataSource;
@GetMapping("/insecure-login")
public String insecureLogin(@RequestParam("username") String username, @RequestParam("password") String password) {
String sql = "SELECT * FROM users WHERE username='" + username + "' AND password=?";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, password);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return "Login successful!";
} else {
return "Invalid username or password!";
}
}
} catch (SQLException e) {
e.printStackTrace();
return "An error occurred during login.";
}
}
}
在这个例子中,我们使用了PreparedStatement来执行查询,将密码作为参数传递给了PreparedStatement,但用户名仍然被拼接到SQL查询语句中,此时进行预编译则无法阻止 SQL 注入的产生
4.MyBatis使用了不安全的${}
MyBatis 中使用 parameterType 向 SQL 语句传参,在 SQL 引用传参可以使用#{Parameter}
和${Parameter}
两种方式。#{Parameter}
采用预编译的方式构造 SQL,避免了 SQL 注入的产生。而${Parameter}
采用拼接的方式构造 SQL,在对用户输入过滤不严格的前提下,此处很可能存在 SQL 注入
例如:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
<select id="getUserByUsernameAndPassword" resultType="com.example.User">
SELECT * FROM users WHERE username = '${username}' AND password = '${password}'
</select>
</mapper>
5.MyBatis order by注入
漏洞原因
order by 子句不能使用参数化查询的方式,只能使用字符拼接的方式,而在MyBatis中#{}
是进行参数化查询的,如果在MyBatis的 order by 子句中使用#{}
,则 order by 子句会失效,所以要使用 order by 子句只能使用${}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
<select id="getUsersOrderedBy" resultType="com.example.User">
SELECT * FROM users
<if test="orderBy != null">
ORDER BY ${orderBy}
</if>
</select>
</mapper>
假设用户输入了以下恶意输入:
orderBy参数:id; DROP TABLE users;
如果将这个恶意输入作为orderBy参数传递给上述示例中的接口,那么生成的SQL查询语句将会是:
SELECT * FROM users ORDER BY id; DROP TABLE users;
这个查询会先按照id字段排序用户表,然后执行一个额外的恶意SQL语句,将users表删除
防御方法
解决order by注入的一个有效方法,就是将order by后面的内容写死,用户不可控,也就触发不了SQL注入了:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
<select id="getUsersOrderedById" resultType="com.example.User">
SELECT * FROM users
ORDER BY id
</select>
</mapper>
6.MyBatis 模糊查询注入
漏洞原因
%
和_
模糊查询,MyBatis的like子句中使用#{}
程序会报错,例如:“select * from users where name like '%#{user}%'
”;为了避免报错只能使用${}
,例如:“select * from users where name like '%${user}%'
”;但${}
可能会存在SQL注入漏洞,要避免SQL注入漏洞就要进行过滤
例如,下面是一个有漏洞的代码:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
<select id="getUserByName" resultType="com.example.User">
SELECT * FROM users WHERE name LIKE '%${name}%'
</select>
</mapper>
假设用户输入了以下恶意输入:
name参数:'; DROP TABLE users; --
如果将这个恶意输入作为name参数传递给上述示例中的接口,那么生成的SQL查询语句将会是:
SELECT * FROM users WHERE name LIKE '%'; DROP TABLE users; -- %'
这个查询会先匹配一个空字符串,然后执行一个额外的恶意SQL语句,将users表删除
防御方法
防御这种类型的攻击,一种有效的方式是强制数据类型,使用 ${}
本身是存在注入的,但由于强制使用Integer或long类型导致注入无效(无法注入字符串)
当然,最有效的方式,还是使用 #{}
安全编码,不过为了语法的正确性,要采用CONCAT
函数进行拼接语句:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
<select id="getUserByName" resultType="com.example.User">
SELECT * FROM users WHERE name LIKE CONCAT('%', #{name}, '%')
</select>
</mapper>
7.MyBatis in 子句注入
漏洞原因
在 MyBatis 的 in 子句中使用#{}
会将多个参数当作一个整体,这偏离了原来的程序设计逻辑,无法查到数据,为了避免这个问题,只能使用${}
:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
<select id="getUsersByIds" resultType="com.example.User">
SELECT * FROM users WHERE id IN (${ids})
</select>
</mapper>
假设用户输入了以下恶意输入:
ids参数:1, 2, 3); DROP TABLE users; --
如果将这个恶意输入作为ids参数传递给上述示例中的接口,那么生成的SQL查询语句将会是:
SELECT * FROM users WHERE id IN (1, 2, 3); DROP TABLE users; --
这个查询会先查询id为1、2、3的用户,然后执行一个额外的恶意SQL语句,将users表删除
防御方法
MyBatis提供了标签,用于安全地处理集合或数组类型的参数。通过标签,可以避免使用${},从而防止SQL注入,例如:
<select id="selectByIds" resultType="java.util.HashMap">
SELECT * FROM my_table WHERE id IN
<foreach item="id" collection="idList" open="(" separator="," close=")">
#{id}
</foreach>
</select>
如果必须通过动态SQL拼接来实现,应该结合MyBatis的动态SQL标签,如、等,尽量避免直接使用${}。如果实在无法避免,也应确保传入的数据已经经过彻底的输入验证和清理
8.JavaSQL注入审计关键词
statement
${
select
update
insert
delete
order