MyBatis动态SQL安全实战:从#{}与${}区别到SQL注入漏洞修复
1. 从一道课后习题到真实的安全漏洞最近在辅导团队新人学习Java EE特别是MyBatis框架时总会让他们做一道经典的课后习题“请简述MyBatis中#{}和${}的区别并说明动态SQL中如何使用。”这道题几乎成了面试和笔试的“八股文”答案也烂熟于心#{}是预编译安全${}是字符串拼接有SQL注入风险动态SQL里要慎用。然而就在上周我们一个已经上线运行了半年的服务突然被公司的安全团队用的正是奇安信的天镜扫描器扫出了一个高危的SQL注入漏洞。定位到代码一看问题恰恰出在一个“以为很安全”的动态SQL片段里一个不经意的${}用法成了罪魁祸首。这让我意识到书本上的“知道”和实战中的“做到”中间隔着一道巨大的鸿沟。这道课后习题远不是背下区别那么简单它直接关联着系统的命门——安全。所以今天我们不只谈区别更想结合这次真实的踩坑经历把动态SQL里那些关于${}的“灰色地带”和“安全红线”彻底讲透。你会看到在哪些场景下你可能会“被迫”或“无意中”用到${}以及如何在这种“高危操作”下依然能构建出铜墙铁壁。2.#{}与${}不仅仅是“安全”与“不安全”的二分法几乎所有教程都会告诉你#{}是占位符MyBatis会将其替换为?然后使用PreparedStatement进行预编译能有效防止SQL注入。而${}是字符串替换MyBatis会将其替换为变量的字面值直接拼接到SQL语句中存在注入风险。这个结论没错但它太绝对了容易让人产生一个误区用了${}就等于有漏洞。实际上风险不取决于你用没用${}而取决于这个${}所替换的内容是否用户可控。2.1 深入原理预编译如何筑起防线为了理解为什么#{}安全我们需要稍微深入一点。当MyBatis处理SELECT * FROM user WHERE id #{userId}时SQL解析与编译数据库驱动会先将SELECT * FROM user WHERE id ?这个模板SQL发送给数据库。数据库会对其进行语法分析、优化并生成一个执行计划。这个“”就是一个等待输入的参数位。参数传递之后驱动再将真实的参数值如userId5单独传递给数据库。关键点无论参数值是什么哪怕是userId “5 or 11”数据库也只会把它当作一个整体的字符串值去和id字段比较。它不会将“or 11”解析为新的SQL操作符因为SQL的结构在第一步就已经固定了。而${}则完全不同。SELECT * FROM user WHERE id ${userId}如果userId是字符串“5 or 11”最终生成的SQL会是SELECT * FROM user WHERE id 5 or 11。这条语句到达数据库时数据库会完整地解析id 5 or 11or 11成了新的查询条件导致查询出所有数据。2.2${}的合法使用场景当你需要“SQL片段”时既然${}这么危险为什么MyBatis还要保留它因为它解决的是#{}无法解决的问题动态拼接SQL语句的组成部分而非参数值。场景一动态表名或列名想象一个数据分表的场景用户数据按年份分表user_2023,user_2024。你想根据年份动态查询不同的表。select idselectByYear resultTypeUser SELECT * FROM user_${year} WHERE status #{status} /select这里${year}替换的是表名的一部分它是一个标识符而不是一个值。#{status}才是查询条件的值。你无法写成FROM user_#{year}因为数据库不允许FROM user_?这种语法。场景二排序字段动态化用户前端点击表头需要按不同字段排序。select idselectUsers resultTypeUser SELECT * FROM user ORDER BY ${orderBy} ${orderType} /select同样ORDER BY ? DESC是非法SQL。排序的列名和顺序ASC/DESC必须是SQL语句的一部分。关键认知转变在这些场景中${}注入的内容不应来自用户直接的、未经验证的输入。上面的${year}应该在你的业务逻辑中严格限定比如从当前日期计算或从一个预定义的枚举中获取${orderBy}应该在前端或后端对传入的字段名进行白名单校验比如只允许“name”, “create_time”等几个字段。3. 动态SQL的“安全区”与“风险区”实战拆解MyBatis的动态SQL标签if,choose,where,set,foreach极大地简化了复杂查询的编写。大部分时候我们在这些标签里混用#{}和${}但风险就藏在混用的细节里。3.1 安全区在动态标签内使用#{}构建条件这是最标准、最安全的用法。动态SQL标签负责逻辑判断#{}负责安全地传入值。select idfindUsers resultTypeUser SELECT * FROM user where if testname ! null and name ! ‘’ AND name #{name} /if if testemail ! null AND email #{email} /if if teststatusList ! null and statusList.size 0 AND status IN foreach collectionstatusList itemstatus open( separator, close) #{status} /foreach /if /where /select在这个例子中即便name来自用户输入因为使用了#{}所以是安全的。foreach标签内遍历集合每个项也用#{}包裹确保了IN查询的安全性。3.2 风险区在动态标签内混用${}这里就是坑开始的地方。我们来看一个我踩过的真实案例的简化版。需求一个后台管理系统需要支持根据管理员选择的“字段”和“关键词”进行动态模糊搜索。字段可选“用户名(name)”或“邮箱(email)”。最初的错误实现select iddynamicSearch resultTypeUser SELECT * FROM user where if testfield ! null and keyword ! null AND ${field} LIKE CONCAT(‘%’, #{keyword}, ‘%’) /if /where /select看起来好像没问题field用了${}因为它是列名keyword用了#{}因为它是值。但问题在于field这个参数是从前端下拉框传过来的。攻击者完全可以绕过前端直接构造HTTP请求将field参数设置为field “name’ OR ‘1’‘1’ -- ” keyword “anything”拼接后的SQL会变成SELECT * FROM user WHERE name’ OR ‘1’‘1’ -- LIKE CONCAT(‘%’, ‘anything’, ‘%’)--是SQL注释符后面的内容被注释掉。最终执行的查询是WHERE name’ OR ‘1’‘1’由于语法错误或者恒真条件可能导致信息泄露或异常。奇安信扫描器正是捕捉到了这种模式它发现你的SQL语句中存在来自请求参数的、未经验证的字符串直接拼接${}即使它当前被用在列名位置扫描器也会保守地判定为潜在注入点报出高危漏洞。从安全角度看这种报法是合理的。4. 漏洞修复从“可用”到“安全”的代码重构收到漏洞报告后修复过程不是简单地把${}换成#{}那会语法错误而是需要建立一套防御机制。4.1 方案一白名单校验最推荐在参数传入MyBatis的XML之前在Java服务层进行严格的校验。public ListUser dynamicSearch(String field, String keyword) { // 定义允许查询的字段白名单 SetString allowedFields new HashSet(Arrays.asList(“name”, “email”)); if (!allowedFields.contains(field)) { // 抛出业务异常记录日志或者使用一个安全的默认值 throw new IllegalArgumentException(“非法的查询字段: “ field); // 或者 field “name”; // 使用默认值 } // 此时field一定是安全的 return userMapper.dynamicSearch(field, keyword); }这样无论前端传来什么到了MyBatis这一层${field}中的field只可能是“name”或“email”彻底杜绝了注入的可能。这是最小权限原则的体现。4.2 方案二使用choose标签枚举所有可能适用于场景少的情况如果动态字段的可能性很少可以直接在XML中用choose写死所有分支完全避免${}。select iddynamicSearch resultTypeUser SELECT * FROM user where choose when test“field ‘name’ and keyword ! null” AND name LIKE CONCAT(‘%’, #{keyword}, ‘%’) /when when test“field ‘email’ and keyword ! null” AND email LIKE CONCAT(‘%’, #{keyword}, ‘%’) /when otherwise !-- 可加11或不加条件视业务而定 -- /otherwise /choose /where /select这个方法将逻辑判断完全收拢在XML中无需在Java代码中校验字段但缺点是如果可选项很多XML会变得冗长。4.3 方案三对${}内容进行转义复杂且不推荐理论上可以对传入的field值进行严格的SQL标识符转义比如检查是否只包含字母、数字、下划线并去除反引号等。但MyBatis本身不提供这个功能需要自己实现且不同数据库的标识符规则略有不同容易留下死角。除非有非常特殊和受控的场景否则不如白名单方案直接可靠。我们最终采用了方案一白名单校验修复后代码清晰安全性也易于理解和审计。重新提交扫描后漏洞状态标记为“已修复”。5. 高级场景下的${}风险与防御除了上述明显的动态列名还有一些更隐蔽的场景。5.1foreach标签中的${}陷阱foreach通常用于IN查询我们一般这样安全地使用AND id IN foreach collection“idList” item“id” open“(” separator“,” close“)” #{id} /foreach但如果你需要动态决定IN查询的字段呢比如按id集合查或者按code集合查。AND ${inField} IN foreach collection“valueList” item“value” open“(” separator“,” close“)” #{value} /foreach看${inField}又出现了同样的必须对inField进行白名单校验“id”, “code”。5.2ORDER BY与动态排序的终极安全写法结合白名单一个安全的动态排序实现如下// Service层 public ListUser getUsers(String orderBy, String orderType) { MapString, String columnMap new HashMap(); columnMap.put(“name”, “name”); columnMap.put(“time”, “create_time”); // 校验并获取安全的数据库列名 String safeOrderBy columnMap.get(orderBy); if (safeOrderBy null) { safeOrderBy “create_time”; // 默认值 } // 校验排序方式 String safeOrderType “ASC”.equalsIgnoreCase(orderType) || “DESC”.equalsIgnoreCase(orderType) ? orderType.toUpperCase() : “DESC”; return userMapper.selectUsers(safeOrderBy, safeOrderType); }!-- Mapper XML -- select idselectUsers resultTypeUser SELECT * FROM user ORDER BY ${safeOrderBy} ${safeOrderType} /select经过Service层的过滤传到XML的${safeOrderBy}和${safeOrderType}已经是绝对安全的内部值了。5.3LIMIT子句的“历史遗留”问题在MySQL中LIMIT子句后的参数不允许使用预编译的占位符即LIMIT ?, ?在某些旧版本驱动或场景下不支持。这是一个历史遗留的“特例”。因此老代码中经常看到LIMIT ${offset}, ${pageSize}这风险极高offset和pageSize通常是数字但攻击者可以传入0; DROP TABLE user --之类的字符串。修复方案强制类型转换与范围校验在Java层确保offset和pageSize是正整数并设置合理上限如每页最多100条。使用MyBatis的RowBounds不推荐用于分页查询性能不佳。最佳实践使用PageHelper等成熟分页插件。这些插件在底层会安全地处理分页参数无需你手动拼接LIMIT。6. 安全开发习惯与代码审计要点经过这次事件我们在团队内推行了几条关于MyBatis动态SQL的硬性规定禁用搜索在IDE和代码仓库中全局搜索${。任何一处的出现都必须经过安全评审说明其必要性和已采取的安全措施如白名单。参数校验前置坚持在Service层或专门的校验器中对所有传入Mapper的参数进行校验特别是用于${}的参数必须进行白名单或强类型转换。代码审查重点在Code Review时动态SQL是必看项。重点关注if、foreach、choose标签内的表达式看是否有未经验证的参数直接用于字符串拼接${}或OGNL表达式注入风险test属性虽然一般安全但也要注意。依赖安全插件在Maven或Gradle中引入find-sec-bugs、SpotBugs等静态代码安全扫描插件并将其集成到CI/CD流程中自动检测潜在的${}误用问题。理解扫描器报告当收到奇安信、Fortify等安全扫描器的报告时不要急于标记“误报”。首先要彻底理解它报出的原因即使当前参数看似可控也要思考未来代码迭代、参数传递路径变化后是否可能失控。最安全的态度是除非能证明绝对安全否则一律视为不安全。那道关于#{}和${}区别的课后习题答案不应该止步于“一个安全一个不安全”。真正的答案是一套结合了白名单校验、最小权限原则、参数前置过滤和严格代码审查的完整防御体系。动态SQL是MyBatis的利器但${}就像是这把利器的锋刃用得好可以披荆斩棘用不好就会伤及自身。记住在安全问题上永远不要心存侥幸也永远不要相信任何未经验证的外部输入。

相关新闻

Unity iOS内购服务器端验证:从原理到Node.js实战,构建安全支付系统

Unity iOS内购服务器端验证:从原理到Node.js实战,构建安全支付系统

1. 项目概述:为什么后台验证是IAP的“生命线”做Unity游戏内购,尤其是面向iOS平台,很多开发者朋友可能觉得,只要在Unity里把IAP插件配好,客户端能弹出购买窗口、能收到购买成功的回调,这事儿就算成了。如果…

2026/7/29 3:52:35阅读更多 →
SpringBoot+Vue2智能垃圾分类系统开发实战

SpringBoot+Vue2智能垃圾分类系统开发实战

1. 项目概述:SpringBoot智能垃圾分类管理系统的核心价值这套基于SpringBootVue2的智能垃圾分类管理系统源码,是我在参与某市智慧社区建设项目时沉淀下来的实战成果。系统通过图像识别与数据联动,实现了从垃圾投放到清运调度的全流程数字化管理…

2026/7/29 3:52:35阅读更多 →
电力电子器件串并联实战:从均压均流原理到IGBT/MOSFET布局避坑指南

电力电子器件串并联实战:从均压均流原理到IGBT/MOSFET布局避坑指南

1. 项目概述:为什么器件串并联是电力电子工程师的必修课?干了十几年电力电子,从做小功率开关电源到搞大功率变频器,有一个话题是绕不开的,那就是器件的串联和并联。乍一看,这好像是个基础得不能再基础的问题…

2026/7/29 3:52:35阅读更多 →
Transformer 3问

Transformer 3问

Transformer(自注意力、多头注意力),衍生:GPT、BERT、LLaMA, 豆包也是吗 结论:豆包是的,底层同样基于 Transformer(自注意力 + 多头注意力),属于Decoder-only 解码器架构,和 GPT、LLaMA 是同一类技术路线,和 BERT 路线不同。 一、先分清 Transformer 两大分支 Tr…

2026/7/29 5:03:27阅读更多 →
蓝桥杯搜索算法实战:从DFS/BFS原理到剪枝优化与真题解析

蓝桥杯搜索算法实战:从DFS/BFS原理到剪枝优化与真题解析

1. 从“暴力枚举”到“智能搜索”:蓝桥杯搜索专题的本质如果你参加过蓝桥杯,或者刷过它的历年真题,大概率会有一个感觉:这比赛怎么这么爱考“搜索”?从最基础的迷宫问题,到复杂的状态空间枚举,搜…

2026/7/29 5:03:27阅读更多 →
C++单例模式实战:从线程安全到现代实现与避坑指南

C++单例模式实战:从线程安全到现代实现与避坑指南

1. 项目概述:为什么单例模式是C工程师的必修课?如果你写过一段时间的C,尤其是在处理一些全局资源,比如日志管理器、配置读取器、数据库连接池或者线程池时,大概率会碰到一个经典问题:如何确保一个类在整个程…

2026/7/29 5:03:27阅读更多 →
敏捷研发项目管理的核心挑战与四维体系实践

敏捷研发项目管理的核心挑战与四维体系实践

1. 研发项目管理的核心挑战作为在软件行业摸爬滚打十年的老兵,我见过太多研发团队在项目管理上栽跟头。上周还遇到个典型case:某创业团队耗时半年开发的SAAS系统,上线后才发现核心功能与客户需求南辕北辙。问题根源就在于用传统"需求-开…

2026/7/29 5:03:27阅读更多 →
Flask视图函数核心原理与实战技巧

Flask视图函数核心原理与实战技巧

1. 为什么视图函数是Flask开发的核心第一次接触Flask时,我像大多数初学者一样被各种概念包围——路由、模板、请求上下文、蓝图...但真正让我理解Flask精髓的,是视图函数(View Function)这个看似简单的概念。它不仅是处理HTTP请求的入口点,更…

2026/7/29 5:03:27阅读更多 →
物联网设备低功耗优化与电池寿命延长技术

物联网设备低功耗优化与电池寿命延长技术

1. 物联网设备电池寿命延长的核心挑战在野外环境监测、工业传感器网络等典型物联网应用中,设备往往需要部署在难以频繁维护的偏远位置。我曾参与过一个高原气象站项目,设备安装在海拔4500米的无人区,更换电池需要专门组织登山队,单…

2026/7/29 5:01:27阅读更多 →
覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

🔹 工具基础介绍 OpenClaw 是开源生态中一款实用性较强的本地智能工具,凭借本地离线运行、可视化图形操作和任务自动化三大核心特性,赢得了众多用户的青睐。与普通在线对话AI工具不同,它属于能够直接操控本机软硬件的智能数字员工…

2026/7/28 4:06:39阅读更多 →
伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

所谓液压伺服阀体的精密激光焊接,是用激光束对阀座壳体(通常为不锈钢或铝合金)进行密封焊接,使阀体在21-35MPa的高压液压油或压缩气体中长期运行而不发生介质泄漏。液压伺服阀是高端液压系统的"大脑"。从航空航天飞行控…

2026/7/28 2:08:06阅读更多 →
D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南 【免费下载链接】d2dx D2DX is a complete solution to make Diablo II run well on modern PCs, with high fps and better resolutions. 项目地址: https://gitcode.com/gh_mirrors/d2/d2dx 你是否还在…

2026/7/28 1:38:28阅读更多 →
28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“!

28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“!

28. Agent 执行到一半想暂停?用 interrupt 给它设个“关卡“! 在构建复杂的 Agent 系统时,我们经常会遇到这样的场景:Agent 正在执行一个多步骤的任务,比如“下单购买商品”,但执行到一半时,我们…

2026/7/29 0:01:46阅读更多 →
自律同行,突破无界!NANK南卡正式官宣曾舜晞成为品牌代言人

自律同行,突破无界!NANK南卡正式官宣曾舜晞成为品牌代言人

近日,国际专注开放式技术研发的声学品牌Nank南卡,正式官宣实力艺人曾舜晞担任品牌代言人。消息一经发出便轰动全网。为什么耳机品牌不选择流量明星、老牌歌手?而且是选择曾舜晞?让我们一起来探索一下!比起短期的流量&a…

2026/7/29 0:01:46阅读更多 →
【RT-DETR多模态创新改进】CVPR 2025 | 独家特征融合创新改进篇 | 引入RLAB残差线性注意力模块,有效融合并强调多尺度特征,多种改进点,适合红外与可见光融合目标检测任务,有效涨点

【RT-DETR多模态创新改进】CVPR 2025 | 独家特征融合创新改进篇 | 引入RLAB残差线性注意力模块,有效融合并强调多尺度特征,多种改进点,适合红外与可见光融合目标检测任务,有效涨点

一、本文介绍 🔥本文在RT-DETR多模态融合目标检测中引入RLAB残差线性注意力模块,可在不同模态特征交互阶段进行多次残差细化,使可见光、红外等特征在尺度、语义和空间位置上更好对齐;随后将细化特征与解码器输出拼接并生成Q、K、V,通过线性注意力自适应强化关键通道、目…

2026/7/29 0:01:46阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/28 20:22:24阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/29 4:31:51阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/28 2:35:58阅读更多 →