SQL注入防护实战:预编译语句与ORM参数化查询方案

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/

(0)
小编小编
上一篇 8小时前
下一篇 8小时前

相关推荐