SQL注入是Web安全中历史最悠久且至今仍高频出现的漏洞类型。OWASP Top 10中SQL注入常年占据前三,每年仍有大量数据泄露事件因SQL注入导致。数据库运维和后端开发中,正确使用预编译语句和参数化查询是防护SQL注入的根本手段。
SQL注入攻击原理与常见场景
SQL注入的本质是将用户输入作为SQL语句的一部分执行。以下是一个典型漏洞示例:
// 漏洞代码 - 字符串拼接SQL
String sql = "SELECT * FROM users WHERE username = '"
+ username + "' AND password = '" + password + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 正常输入: username=admin, password=123456
// SELECT * FROM users WHERE username='admin' AND password='123456'
// 恶意输入: username=admin'--, password=anything
// SELECT * FROM users WHERE username='admin'--' AND password='anything'
// -- 后面的内容被注释,密码校验被绕过
常见注入类型:联合查询注入(UNION-based)通过UNION拼接查询窃取其他表数据;布尔盲注通过AND 1=1/AND 1=2判断条件真假逐字符猜解;时间盲注通过SLEEP()函数响应时间差异提取数据;堆叠注入通过分号执行多条SQL。
预编译语句防护原理与实现
预编译语句(PreparedStatement)的防护原理是SQL语句结构与数据分离。SQL语句在预编译阶段完成语法解析和执行计划生成,用户输入只作为参数值绑定,不会被解释为SQL语法。
// 正确写法 - 使用PreparedStatement参数化
String sql = "SELECT id, username, email FROM users "
+ "WHERE username = ? AND status = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setInt(2, 1);
ResultSet rs = pstmt.executeQuery();
// 即使输入 admin'-- 也不会产生注入
// 数据库将其视为字面字符串值,而非SQL语法
预编译语句的三个阶段:1)prepare阶段,数据库解析SQL模板生成执行计划;2)execute阶段,参数值替换占位符并执行;3)close阶段,释放资源。同一个PreparedStatement可重复execute只更换参数值,执行计划复用提高性能。
IN查询的参数化处理是常见痛点,IN子句不能直接用单个占位符:
// 错误写法 - IN子句拼接
String sql = "SELECT * FROM products WHERE id IN (" + ids + ")";
// ids = "1,2,3) UNION SELECT password FROM users--" -> 注入
// 正确写法 - 动态生成占位符
List<Integer> idList = Arrays.asList(1, 2, 3);
String placeholders = idList.stream()
.map(i -> "?")
.collect(Collectors.joining(","));
String sql = "SELECT * FROM products WHERE id IN (" + placeholders + ")";
PreparedStatement pstmt = conn.prepareStatement(sql);
for (int i = 0; i < idList.size(); i++) {
pstmt.setInt(i + 1, idList.get(i));
}
ResultSet rs = pstmt.executeQuery();
LIKE模糊查询也需要注意参数化方式:
// 错误写法
String sql = "SELECT * FROM articles WHERE title LIKE '%" + keyword + "%'";
// 正确写法 - 使用CONCAT拼接通配符
String sql = "SELECT * FROM articles WHERE title LIKE CONCAT('%', ?, '%')";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, keyword);
ORM框架参数化查询实践
ORM框架并非自动免疫SQL注入。正确使用参数化API才安全,错误使用原生查询拼接同样存在风险。
MyBatis中使用#{}占位符是安全的(底层使用PreparedStatement),使用${}是危险的(直接字符串替换):
<!-- 安全写法 - 使用#{}预编译 -->
<select id="getUserByName" resultType="User">
SELECT id, username, email FROM users
WHERE username = #{username}
</select>
<!-- 危险写法 - 使用${}字符串拼接 -->
<select id="getUserByName" resultType="User">
SELECT id, username, email FROM users
WHERE username = '${username}'
</select>
<!-- ORDER BY动态排序需代码层白名单校验 -->
<select id="queryProducts" resultType="Product">
SELECT * FROM products
<if test="orderField != null">
ORDER BY ${orderField} ${orderDirection}
</if>
</select>
<!-- 代码层校验:
String[] allowedFields = {"price", "created_at", "name"};
if (!Arrays.asList(allowedFields).contains(orderField)) {
throw new IllegalArgumentException("非法排序字段");
}
-->
JPA/Hibernate中使用命名参数或位置参数:
// JPA TypedQuery - 安全
TypedQuery<User> query = em.createQuery(
"SELECT u FROM User u WHERE u.username = :name AND u.status = :status",
User.class
);
query.setParameter("name", username);
query.setParameter("status", UserStatus.ACTIVE);
List<User> users = query.getResultList();
// Native Query - 仍需参数化
Query nativeQuery = em.createNativeQuery(
"SELECT * FROM users WHERE email = ?1"
);
nativeQuery.setParameter(1, email);
输入验证与纵深防御策略
参数化查询是第一道防线,纵深防御需要多层配合。输入验证层在请求入口对参数做类型检查、长度限制和格式校验:
public class InputValidator {
private static final Pattern USERNAME_PATTERN =
Pattern.compile("^[a-zA-Z0-9_]{3,32}$");
private static final Pattern ID_PATTERN =
Pattern.compile("^\d{1,19}$");
public static String sanitizeUsername(String input) {
if (input == null || input.length() > 32) {
throw new IllegalArgumentException("用户名长度非法");
}
if (!USERNAME_PATTERN.matcher(input).matches()) {
throw new IllegalArgumentException("用户名格式非法");
}
return input;
}
public static Long validateId(String idStr) {
if (!ID_PATTERN.matcher(idStr).matches()) {
throw new IllegalArgumentException("ID格式非法");
}
Long id = Long.parseLong(idStr);
if (id <= 0) throw new IllegalArgumentException("ID必须为正数");
return id;
}
}
@PostMapping("/users/search")
public List<User> searchUsers(@RequestParam String username) {
String safeName = InputValidator.sanitizeUsername(username);
return userService.findByName(safeName);
}
数据库层面额外防护措施:应用账号最小权限原则,业务账号不授予DBA权限,禁用FILE权限(防止INTO OUTFILE写文件攻击),禁用多语句执行(allowMultiQueries=false)。配置WAF规则拦截常见注入特征(UNION SELECT、SLEEP()、BENCHMARK()等)。定期使用SQLMap等工具对内部接口做安全扫描。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/sql-zhu-ru-fang-hu-shi-zhan-yu-bian-yi-yu-ju-yu-orm-can-shu/