MyBatis ${} 与 #{} 的 SQL 注入风险对比:从原理到实战防御

在 Java 持久层框架中,MyBatis 凭借其灵活的 SQL 映射能力,成为众多企业级项目的首选。然而,正是这种灵活性,让一个看似微小的符号选择——${}#{}——成为 SQL 注入漏洞的温床。本文将从底层原理出发,对比两者的注入风险,并结合 XSS 防御、MySQL 安全写法及 Linux 服务器加固,给出一套完整的 Web 安全实践清单。

一、#{}${} 的本质区别

MyBatis 在处理 SQL 参数时,对 #{}${} 采用了完全不同的机制:

  • #{}:预编译参数占位符
    MyBatis 会将 #{} 替换为 JDBC 的 ?,然后使用 PreparedStatementsetXxx() 方法传入参数。最终执行的 SQL 中,参数值被当作纯数据处理,不会改变 SQL 语义。

  • ${}:字符串拼接替换
    MyBatis 直接将 ${} 替换为传入的字符串字面量,然后进行 SQL 编译。这意味着用户输入可以直接拼接到 SQL 语句中,从而改变原有逻辑。

示例对比:

<!-- 安全:预编译 -->
SELECT * FROM users WHERE username = #{username}

<!-- 危险:字符串拼接 -->
SELECT * FROM users WHERE username = '${username}'

若用户输入 ' OR '1'='1,使用 ${} 的语句会变成:

SELECT * FROM users WHERE username = '' OR '1'='1'

结果就是绕过认证,泄露全部用户数据。

二、${} 的典型注入场景

${} 并非一无是处,它常用于动态表名、列名、排序字段(ORDER BY)等无法使用预编译的位置。但正是这些场景,极易被攻击者利用:

  1. ORDER BY 注入
    ORDER BY ${sortField} 中,攻击者可传入 (CASE WHEN (SELECT ...) THEN 1 ELSE 2 END) 进行盲注。

  2. 表名/列名注入
    SELECT * FROM ${tableName},攻击者可传入 users WHERE 1=1 -- 篡改查询范围。

  3. LIKE 查询误用
    虽然 LIKE '%${keyword}%' 看似只是模糊匹配,但攻击者可通过 %' AND 1=1 -- 实现注入。

三、防御策略:如何安全使用 MyBatis

1. 优先使用 #{},杜绝不必要的 ${}

任何用户可控的参数,只要位于 SQL 的值位置,必须使用 #{}。这是最根本的原则。

2. 必须使用 ${} 时,采用白名单校验

对于排序字段、表名等,不要直接拼接用户输入,而应通过白名单映射:

// 定义允许的排序字段
private static final Set<String> ALLOWED_SORT_FIELDS = Set.of("id", "username", "created_at");

public String getSafeSortField(String input) {
    if (!ALLOWED_SORT_FIELDS.contains(input)) {
        throw new IllegalArgumentException("Invalid sort field");
    }
    return input;
}

然后在 XML 中使用 ${safeSortField},且该值来自服务端白名单,而非原始用户输入。

3. 使用 MyBatis 内置的 bind 标签

对于 LIKE 查询,可用 bind 构造参数:

<select id="searchUsers" resultType="User">
    <bind name="pattern" value="'%' + keyword + '%'" />
    SELECT * FROM users WHERE username LIKE #{pattern}
</select>

4. 全局配置与代码审计

  • mybatis-config.xml 中开启 logImpl 记录 SQL,便于审计。
  • 使用静态代码分析工具(如 SonarQube)扫描 ${} 的使用。
  • 禁止在 Mapper XML 中出现 SELECT * FROM ${table} 这类无校验拼接。

四、扩展:Web 安全防御清单

SQL 注入只是 Web 安全的一环。结合你提到的其他主题,以下是一份综合防御清单:

1. XSS 的常见方式与前端防御

常见方式:反射型(URL 参数)、存储型(评论区、用户资料)、DOM 型(innerHTMLeval)。

前端防御

  • 输出编码:根据上下文使用 HTML 实体编码、JavaScript 转义、URL 编码。
  • 使用 CSP(内容安全策略)限制脚本来源。
  • 避免 innerHTML,改用 textContent 或框架的自动转义(如 React、Vue)。
  • 对富文本使用 DOMPurify 等库过滤。

2. MySQL 防止 SQL 注入的几种写法

  • 预处理语句PREPARE stmt FROM 'SELECT * FROM users WHERE id = ?'
  • 存储过程:参数化调用,避免动态拼接。
  • 最小权限原则:应用数据库账户只授予必要权限,禁用 FILEDROP 等。
  • 转义函数mysql_real_escape_string()(仅作最后防线,不推荐替代预处理)。

3. Linux 服务器安全加固清单

  • 更新系统与软件补丁,启用自动安全更新。
  • 禁用 root 远程登录,使用 SSH 密钥认证,修改默认端口。
  • 配置防火墙(iptables/firewalld/ufw),仅开放必要端口。
  • 安装 Fail2Ban 防止暴力破解。
  • 定期审计日志(/var/log/auth.log/var/log/secure)。
  • 使用 SELinux 或 AppArmor 限制服务权限。
  • 对 Web 目录设置正确权限,禁止上传目录执行脚本。

五、总结

MyBatis 的 #{}${} 之争,本质是“数据”与“代码”的边界问题。#{} 将用户输入视为数据,${} 则可能将其变为代码。在 SQL 注入防御中,永远不要信任用户输入,优先使用预编译,对必须拼接的场景实施白名单。同时,将 XSS 防御、MySQL 安全配置、Linux 加固纳入整体安全体系,才能构建真正健壮的 Web 应用。

安全不是一次性的任务,而是贯穿开发、运维、审计的持续过程。从每一个 #{} 开始,守住第一道防线。

未经允许不得转载:任鹏个人博客 » MyBatis ${} 与 #{} 的 SQL 注入风险对比:从原理到实战防御

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏