代码审计-SQL注入
背景知识:SQL 注入是什么?
- SQL 注入发生在应用程序将用户输入直接拼接到 SQL 语句中,导致输入被当作代码(而非数据)执行。
- 例如,在登录功能中,如果恶意用户输入
admin' --
,它可能改变 SQL 逻辑,让攻击者无需密码就能登录。 - 为什么危险? 可能导致数据泄露、数据篡改,甚至整个数据库被破坏。
JDBC
1. 安全做法:预编译语句 + 参数化查询
String sql = "SELECT * FROM users WHERE username = ?"; // SQL 模板,使用 ? 作为占位符
PreparedStatement stmt = conn.prepareStatement(sql); // 预编译这个 SQL 模板
stmt.setString(1, username); // 参数绑定:将 username 输入安全地设置到第一个 ? 的位置
- 为什么安全?
- 预编译(PreparedStatement):数据库(如 MySQL、PostgreSQL)先将 SQL 语句编译成一个“模板”(只包含结构,如
SELECT * FROM users WHERE username = ?
)。数据库知道?
是占位符,不是代码。 - 参数绑定(setString):用户输入(如
username
)在编译后被绑定到占位符。数据库引擎会处理输入,确保它被转义或作为纯数据处理(例如,输入admin' --
会被转义成安全字符串,而不是改变 SQL 逻辑)。 - 隔离输入和指令:用户输入永远不会成为 SQL 代码的一部分,而是作为“数据”传递。这样,攻击者无法通过输入修改 SQL 结构。
- 预编译(PreparedStatement):数据库(如 MySQL、PostgreSQL)先将 SQL 语句编译成一个“模板”(只包含结构,如
- 关键点:始终使用
?
占位符和setXXX
方法(如setString
,setInt
)来绑定参数。 - 效果:能有效防御 SQL 注入,是行业最佳实践。
2. 高危漏洞:直接拼接用户输入
String sql = "SELECT * FROM users WHERE id = '" + id + "'"; // 直接拼接用户输入
// 例如,如果 id 是用户输入 "1' OR '1'='1",则 SQL 变成: SELECT * FROM users WHERE id = '1' OR '1'='1'
// 这会导致返回所有用户数据,因为 '1'='1' 永远成立!
- 为什么危险?
- 直接拼接:用户输入(
id
)被直接插入 SQL 字符串中。如果输入包含恶意代码,它会成为 SQL 的一部分。 - 注入示例:如果用户输入
1' OR '1'='1
,整个 SQL 变成SELECT * FROM users WHERE id = '1' OR '1'='1'
。这不再是查询特定 ID,而是返回所有数据(因为'1'='1'
总是真)。攻击者甚至可以输入更危险的语句,比如1'; DROP TABLE users; --
,导致数据被删除。 - 风险根源:输入和代码没有隔离。数据库引擎将整个 SQL 字符串当作一个命令解析,无法区分用户输入和真实代码。
- 直接拼接:用户输入(
- 关键点:任何直接拼接用户输入到 SQL 字符串中的方式都是极高风险的,必须避免。
3. 无效防护:预编译后拼接(预编译无实际效果)
String sql = "SELECT * FROM users WHERE username='" + username + "'"; // 先拼接输入到 SQL 字符串
PreparedStatement stmt = conn.prepareStatement(sql); // 然后预编译这个完整的 SQL 字符串
- 为什么无效?
- 顺序问题:这里,用户输入
username
在调用prepareStatement
之前就被拼接到 SQL 字符串中。例如,如果输入admin' --
,SQL 字符串会变成SELECT * FROM users WHERE username='admin' --'
(--
是 SQL 注释符,会使后面的条件失效)。 - 预编译的作用?
PreparedStatement
的预编译只发生在 SQL 字符串传递给它之后。但这时,SQL 字符串已经包含用户输入(没有占位符),所以数据库引擎会直接解析和执行这个完整字符串。 - 为什么无防护效果? 预编译只是告诉数据库“这个 SQL 可以重用”,但它无法改变 SQL 内容。如果 SQL 在预编译前就包含恶意输入,输入就成了 SQL 代码的一部分,导致注入漏洞。参数化查询的核心是占位符
?
,而这里根本没有使用它,所以PreparedStatement
只是被误用。
- 顺序问题:这里,用户输入
- 简单比喻:就好比你用 Word 写文档。安全做法是先写模板“Hello [姓名]”,然后在打印前填入名字;无效做法是直接写“Hello John”后再告诉 Word 模板化它——名字已经写进去了,无法防止恶意修改。
- 关键点:预编译语句只有在使用参数化查询(即 SQL 中有
?
占位符)时才有效。如果 SQL 字符串在创建前就拼接输入,预编译是徒劳的。
Mybatis
1.使用 ${}
直接拼接
<!-- 高危!直接拼接用户输入 -->
<select id="findUsers" resultType="User">SELECT * FROM ${tableName} WHERE username = '${username}'
</select>
- 风险:相当于直接拼接 SQL
- 审计关键:全局搜索
${
语法
2.ORDER BY 动态排序漏洞
<!-- 高危!直接拼接列名 -->
ORDER BY ${sortColumn} ${sortDirection}
- 风险:输入列名为
id; DROP TABLE users--
可注入 - 安全替代:白名单验证列名
private static final List<String> SAFE_COLUMNS = Arrays.asList("id", "name");
if (!SAFE_COLUMNS.contains(sortColumn)) {throw new InvalidColumnException();
}
3.IN 语句错误处理
// Java代码
List<User> findUsersInIds(@Param("ids") String ids);<!-- 错误方式:直接拼接 -->
SELECT * FROM users WHERE id IN (${ids})
安全方式:
SELECT * FROM users WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}
</foreach>
4.LIKE 语句漏洞
<!-- 错误方式 -->
WHERE name LIKE '%${keyword}%'安全方式:
WHERE name LIKE CONCAT('%', #{keyword}, '%')
作为代码审计新手的建议
-
定位所有SQL执行点:
JDBC:搜索 .createStatement(、.prepareStatement(、.executeQuery(、.executeUpdate( MyBatis:检查 XML Mapper 文件中的 <select>、<insert>、<update>、<delete> 标签
-
识别用户输入源:
request.getParameter() // HTTP参数 request.getHeader() // HTTP头 cookies.getValue() // Cookie session.getAttribute() // Session new String(byteArray) // 二进制数据转换
-
验证防护机制:
- 是否存在直接字符串拼接?
- 是否使用
?
占位符? setXXX
方法是否覆盖所有参数?- 是否错误地在预编译后拼接?
-
检查ORM框架使用:
- Hibernate:检查是否使用
.createQuery()
+参数绑定 - JPA:检查是否使用
@Query
+:param
命名参数 - MyBatis:检查是否使用
#{}
而非${}
- Hibernate:检查是否使用
- 通用安全原则:
- 绝对避免在 SQL 中直接拼接用户输入。
- 始终采用参数化查询(占位符 + 绑定)。
- 使用 ORM 框架(如 Hibernate)可以进一步简化安全查询,但也需注意配置错误。
- 学习资源:如果你是代码审计新手,推荐阅读 OWASP SQL Injection Prevention Cheat Sheet(免费在线文档),或使用工具如 SonarQube 自动扫描漏洞。