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的利器但${}就像是这把利器的锋刃用得好可以披荆斩棘用不好就会伤及自身。记住在安全问题上永远不要心存侥幸也永远不要相信任何未经验证的外部输入。

相关新闻

Python列表全解析:从基础操作到高级特性与性能优化

Python列表全解析:从基础操作到高级特性与性能优化

1. 列表到底是什么?从“购物清单”到内存地址刚接触Python那会儿,我对“列表”这个概念也迷糊过。书上说它是个“有序的可变序列”,听起来很学术。后来我把它想象成一个可以随时修改的购物清单,一下子就通了。你写在纸上的购物清单…

2026/7/29 3:54:48 阅读更多 →
电子电气架构---车载诊断售后发展白皮书(上)

电子电气架构---车载诊断售后发展白皮书(上)

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 假若你的生活不够好,不够努力,那么,加油努力吧,不要抱怨,起而行,迎头赶上,方是正途。假若你已经拥有很多,却依然活得不快乐,那么,让自己慢…

2026/7/29 3:54:48 阅读更多 →
迪文串口屏开发全攻略:从协议解析到单片机交互实践

迪文串口屏开发全攻略:从协议解析到单片机交互实践

1. 项目概述:为什么选择迪文串口屏作为嵌入式HMI的起点?如果你正在为单片机项目寻找一个简单、可靠且性价比高的显示交互方案,那么“迪文串口屏”这个名字大概率已经进入了你的视野。它不像那些需要驱动复杂RGB接口的TFT屏,也不像…

2026/7/29 3:53:47 阅读更多 →

最新新闻

Python 入门第二天:条件判断与循环语句

Python 入门第二天:条件判断与循环语句

学习完变量、字符串、输入输出和类型转换后,我们已经可以编写一些简单的程序了。不过,这些程序通常只能按照固定顺序从上到下运行。现实中的程序需要根据不同情况执行不同操作。例如,判断用户是否成年、根据身高决定是否购票,或者…

2026/7/29 4:09:53 阅读更多 →
AXI总线协议详解:从通道分离到系统集成,掌握SoC/FPGA高效互连设计

AXI总线协议详解:从通道分离到系统集成,掌握SoC/FPGA高效互连设计

1. 从总线到协议:为什么我们需要AXI?在数字电路设计,尤其是SoC(片上系统)和FPGA开发领域,工程师们经常面临一个核心挑战:如何让芯片内部的各种功能模块高效、可靠地“对话”?无论是处…

2026/7/29 4:09:53 阅读更多 →
C语言数组内存原理与实战技巧

C语言数组内存原理与实战技巧

1. 数组是什么?从内存角度理解当你在C语言中写下int arr[5];这行代码时,计算机究竟在背后做了什么?让我们把镜头拉近到内存层面。想象你的内存就像一栋公寓楼,每个房间都有一个固定大小的空间(通常是4字节存放int类型数…

2026/7/29 4:09:53 阅读更多 →
Azkaban SSL/TLS证书验证失败:certificate_unknown错误深度解析与实战解决方案

Azkaban SSL/TLS证书验证失败:certificate_unknown错误深度解析与实战解决方案

1. 项目概述:当Azkaban遇上SSL/TLS证书验证如果你正在使用Azkaban这个流行的开源工作流调度系统,并且尝试让它通过HTTPS与外部服务(比如Hadoop集群的WebHDFS、Spark History Server,或者一个内部的REST API)进行安全通…

2026/7/29 4:09:53 阅读更多 →
AI教材生成:低查重率与高质量内容的三层过滤体系

AI教材生成:低查重率与高质量内容的三层过滤体系

1. 项目背景与核心痛点去年帮某教育机构做内容优化时,他们扔给我一个棘手需求:要在两周内生成整套Python入门教材,且查重率必须低于15%。试了市面上所有AI工具,生成的教材要么查重爆表(普遍30%)&#xff0c…

2026/7/29 4:09:53 阅读更多 →
基于Arduino与红外传感的智能感应洗手液机DIY全攻略

基于Arduino与红外传感的智能感应洗手液机DIY全攻略

1. 从“小P孩”到“家庭守护者”:一个感应洗手液机的诞生记你有没有经历过这样的场景:家里的小朋友,每次洗手都像是一场“战斗”?要么是够不着高高的洗手液瓶,要么是挤得满手都是,最后还得你跟在后面收拾“…

2026/7/29 4:08:53 阅读更多 →

日新闻

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

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

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

2026/7/29 0:00:23 阅读更多 →
AI编程系列02:合并知识功能,给 AI 问数和 RAG 场景打基础

AI编程系列02:合并知识功能,给 AI 问数和 RAG 场景打基础

AI编程系列02:合并知识功能,给 AI 问数和 RAG 场景打基础 在上一期「AI编程系列」中,我们学习了如何构建一个基础的 AI 问答系统,通过简单的输入输出让模型回应问题。但现实世界中的 AI 应用往往需要处理更复杂的场景:…

2026/7/29 0:00:23 阅读更多 →
AI智能体开发实战:从工具调用到企业级部署

AI智能体开发实战:从工具调用到企业级部署

1. 从被动问答到主动执行:AI Agent的范式转变过去两年,大语言模型最显著的应用形态是聊天机器人——用户提问,AI回答。但真正的生产力革命发生在2023年下半年:当AI学会主动调用工具完成任务时,生产力工具的历史被彻底改…

2026/7/29 0:00:23 阅读更多 →

周新闻

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 数据集6000张 完整源码已标注数据集训练好的模型环境配置教程程序运行说明文档,可以直接使用!系统支持图片、视频、摄像头等多种方式检测裂缝,功能强大实用。 1数据集6000张 8各类别

2026/7/28 12:04:22 阅读更多 →
深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

pubg数据集 精选原图1.42万数据 1.49万标签 无任何重复、算法增强或冗余图像! pubg绝地求生目标检测数据集 1分类:e_body,14905个标签,txt格式 共计14244张图,99%为640*640尺寸图像 适合yolo目标检测、AI训练关键词&am…

2026/7/28 8:29:16 阅读更多 →
Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex检测数据集数据集详情检测类别: allies enemy tag图片总量:7247张训练集:5139张验证集:1425张测试集:683张标注状态:全部已标注,即拿即用数据格式:支持YOLO格式及其他格式&#…

2026/7/28 5:03:42 阅读更多 →

月新闻