在 Java 持久层框架中,MyBatis 凭借其灵活的 SQL 映射能力,成为众多企业级项目的首选。然而,正是这种灵活性,让一个看似微小的符号选择——${} 与 #{}——成为 SQL 注入漏洞的温床。本文将从底层原理出发,对比两者的注入风险,并结合 XSS 防御、MySQL 安全写法及 Linux 服务器加固,给出一套完整的 Web 安全实践清单。
一、#{} 与 ${} 的本质区别
MyBatis 在处理 SQL 参数时,对 #{} 和 ${} 采用了完全不同的机制:
-
#{}:预编译参数占位符
MyBatis 会将#{}替换为 JDBC 的?,然后使用PreparedStatement的setXxx()方法传入参数。最终执行的 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)等无法使用预编译的位置。但正是这些场景,极易被攻击者利用:
-
ORDER BY 注入
ORDER BY ${sortField}中,攻击者可传入(CASE WHEN (SELECT ...) THEN 1 ELSE 2 END)进行盲注。 -
表名/列名注入
如SELECT * FROM ${tableName},攻击者可传入users WHERE 1=1 --篡改查询范围。 -
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 型(innerHTML、eval)。
前端防御:
- 输出编码:根据上下文使用 HTML 实体编码、JavaScript 转义、URL 编码。
- 使用 CSP(内容安全策略)限制脚本来源。
- 避免
innerHTML,改用textContent或框架的自动转义(如 React、Vue)。 - 对富文本使用 DOMPurify 等库过滤。
2. MySQL 防止 SQL 注入的几种写法
- 预处理语句:
PREPARE stmt FROM 'SELECT * FROM users WHERE id = ?'。 - 存储过程:参数化调用,避免动态拼接。
- 最小权限原则:应用数据库账户只授予必要权限,禁用
FILE、DROP等。 - 转义函数:
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 注入风险对比:从原理到实战防御


朋友圈点赞图在线生成源码