ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CVE-2023-25330漏洞解读:MybatisPlus多租户插件SQL注入风险与修复

2026/10/1 14:07:26 拓冰建站 浏览量
CVE-2023-25330漏洞解读:MybatisPlus多租户插件SQL注入风险与修复 我的依赖扫描工具突然弹出一条高危告警开发群里的第一反应都是问这句话MybatisPlus 真的出了 SQL 注入漏洞CVE-2023-25330这个名字看起来陌生但它指向的是很多项目每天都在用的多租户插件 TenantLineInnerInterceptor。我当时没有急着找 PoC而是先把三件事查清楚影响面在哪、项目里到底有没有用受影响的模块、修复版本是什么。三件事理顺之后结论其实很明确升级到 3.5.3.2 及以上基本就能解决但在动手改依赖坐标之前你最好先弄明白漏洞是怎么被绕过的否则就算升完级也可能在别的地方踩到同类问题。这篇文章我不打算堆原理术语就按我实际处理的节奏来写先判断影响再讲根因接着给升级步骤和临时兜底方案最后说几个实操中容易翻车的细节。1. 漏洞通告出来后先冷静确认你的项目是不是真的在射程里CVE-2023-25330 是 MybatisPlus 的一个高危漏洞但它并不是全模块通杀那种类型。这个漏洞出在多租户插件TenantLineInnerInterceptor的 SQL 解析与改写逻辑里只有在项目启用了多租户功能、并且 MybatisPlus 版本处于受影响区间时攻击者才有机会通过构造特殊 SQL 绕过租户条件进而访问甚至篡改其他租户的数据。我当时的第一步是查版本。当前的 Maven 仓库和项目 POM 文件全部翻了一遍把所有com.baomidou:mybatis-plus-*相关的依赖找出来记录版本号。这次受影响的是 3.5.3.1 以及更早的版本官方在 3.5.3.2 中修复了问题。所以判断逻辑很直接版本低于 3.5.3.2同时代码里出现了TenantLineInnerInterceptor那就要立刻进入修复流程。1.1 先对号入座检查多租户插件有没有真的被启用很多项目虽然在依赖里引入了 MybatisPlus但并不一定用了多租户插件。你需要确认的是代码里有没有这样一类配置MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() { Override public Expression getTenantId() { return new LongValue(TenantContext.getTenantId()); } Override public String getTenantIdColumn() { return tenant_id; } }));如果项目里搜不到TenantLineInnerInterceptor的实例化代码也没有在MybatisPlusInterceptor的插件链上添加过它那这个 CVE 的实际攻击面就很窄。不过我的建议是即便没启用多租户插件也照样升级到修复版本因为依赖里的漏洞扫描还会持续报警早晚要处理。1.2 确认版本和依赖来源还有一类特殊情况容易被漏掉项目本身没有直接声明 MybatisPlus 依赖但它被公司内部的某个基础框架间接引入或者通过聚合 POM 统一管理。这种情况下你直接改项目里的pom.xml可能没用真正的版本号在父 POM 里。我的检查命令是mvn dependency:tree -Dincludescom.baomidou这条命令会把所有引入的 MybatisPlus 模块列出来包括它们是直接依赖还是传递依赖、实际生效的版本号是多少。如果你用的是 Gradle就用gradle dependencies --configuration runtimeClasspath | grep mybatis-plus把这两条命令的输出保存下来修完后再跑一次对比确认版本真的变了。这一步看似简单但很多升级事故都是因为改了 POM 但传递依赖没生效引起的。2. 漏洞根因拆解TenantLineInnerInterceptor 的租户条件是怎么被绕过的要理解这个漏洞的修复思路得先理解多租户插件做了什么。它在应用层帮你拦截 SQL自动追加租户条件避免你每一条 SQL 都手动写where tenant_id ?。但自动改写 SQL这件事本质上依赖一个 SQL 解析器把语句拆成语法树再在语法树上做插入操作。整个链路中只要有一个环节没有覆盖到就可能出现绕过。2.1 多租户插件的自动改写过程假设你有一条最简单的查询语句SELECT * FROM orders WHERE status 1多租户插件在解析后会自动把它改写成SELECT * FROM orders WHERE status 1 AND tenant_id 10update 和 delete 也是同理会在执行前自动带上租户条件。这个机制在常规场景下非常可靠因为你不用再担心开发人员漏写租户过滤数据库层也不会把不同租户的数据混在一起。但关键在于SQL 像自然语言一样表达方式太多了。一个稳定可靠的解析器需要覆盖 SELECT、INSERT、UPDATE、DELETE、JOIN、子查询、UNION、WITH 等几乎所有语法分支任何一个分支处理不完整都会留下一条没被改写的缝隙。2.2 攻击者利用的是哪些解析薄弱点CVE-2023-25330 的问题就出在解析器对部分复杂 SQL 语法结构的处理上。攻击者并不需要什么高级技巧他只要让插件在改写时漏掉真正执行数据读取的那段 SQL比如把查询藏进特定的子查询结构、UNION 分支或 JOIN 关联条件里插件往语法树上插入的租户条件根本没有覆盖到目标位置租户隔离就直接失效了。如果再配合 Mybatis 自身的动态 SQL 能力把外部传入的恶意片段拼进 SQL 里就有机会进一步升级成真正的注入点。这里我不展开具体的构造语句一方面是没有必要另一方面是这类利用方式一旦被当作学习资料传播风险反而更高。你只要理解它的本质插件加租户条件这件事严重依赖 SQL 解析器对语句的完整覆盖而解析器对某些特殊写法是存在疏漏的。2.3 有租户插件兜底为什么还会出生产事故很多人写代码时有一个习惯既然框架自动帮我加了租户条件那我 WHERE 条件里把用户传来的参数直接拼进去也没问题吧做了一个不太恰当的类比这就像家里装了防盗门就觉得窗户可以不锁了。实际上TenantLineInnerInterceptor 解决的是租户隔离问题不是SQL 注入问题。如果业务代码里使用${}拼接字符串或者把外部输入直接放进 order by 的字段名里SQL 注入的风险依然存在。这个 CVE 正好暴露了插件在部分场景下的薄弱点所以你要明白安全是分层设计不能把一个组件当成所有问题的答案。3. 最省心的修复把 MybatisPlus 升到 3.5.3.2 及以上处理这类漏洞优先级最高的永远是升级到修复版本。MybatisPlus 官方在 3.5.3.2 中修复了 CVE-2023-25330底层主要做了两件事升级了 jsqlparser 版本增强了对复杂 SQL 的解析能力同时调整了 TenantLineInnerInterceptor 对子查询、UNION 等语法结构的改写逻辑。3.1 Maven 和 Gradle 的依赖坐标怎么改如果是普通的 Spring Boot 2 项目直接修改 mybatis-plus-boot-starter 的版本号就行dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency如果你的项目不是用 starter而是直接依赖扩展包和核心包那就分别改dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-extension/artifactId version3.5.3.2/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-core/artifactId version3.5.3.2/version /dependencyGradle 项目对应改implementation com.baomidou:mybatis-plus-boot-starter:3.5.3.2如果你管理的项目很多推荐用 properties 统一控制版本properties mybatis-plus.version3.5.3.2/mybatis-plus.version /properties后面所有依赖里引用${mybatis-plus.version}以后再有升级需求只改一处。3.2 升级时最容易翻车的版本兼容问题升级本身不难难的是升级后行为变化带来的兼容问题。我实际遇到的两个高频问题第一个是 Spring Boot 3 项目的依赖坐标完全不同。Spring Boot 3 用的不是mybatis-plus-boot-starter而是mybatis-plus-spring-boot3-starter坐标长这样dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.3.2/version /dependency如果拿着旧的 starter 坐标硬套到 Spring Boot 3 项目上启动阶段就会报出一堆兼容性错误。第二个是 MybatisPlus 升级后它依赖的 jsqlparser 版本也变了这会导致某些原本虽然写法不标准但还能跑的 SQL突然解析失败。尤其是在配置了多租户插件和分页插件的情况下特殊 SQL 报错的概率明显增加。后面我会单独讲这类问题的排查思路。3.3 升级后怎么确认依赖真的生效改完依赖后我习惯用三步来验证mvn clean compile mvn dependency:tree -Dincludescom.baomidou编译通过说明依赖坐标没问题dependency:tree输出里能看到所有 MybatisPlus 模块的版本还是 3.5.3.1那说明你改的这个 POM 没有真正生效需要继续找父级依赖或 BOM 定义。然后启动应用看启动日志里的 MybatisPlus banner。新版本在启动时打出的 banner 通常保留了版本号看到 3.5.3.2 就可以放心了。如果想要更彻底可以在某个多租户接口执行一次 SQL把 MybatisPlus 的 SQL 日志打开确认tenant_id ?条件仍然存在。4. 一时半会升不了级先上这几道临时防线实际情况里不是所有团队都能立即升级。比如项目中使用了大量 MybatisPlus 内置能力升级后需要全量回归或者有其他团队维护的中间件依赖了旧版本短时间内协调不动。这种情况下我建议做几层临时兜底降低漏洞被利用的概率。4.1 外部入口先拦一道网关和过滤器的参数检测在系统入口处加一个 SQL 注入检测过滤器拦截明显可疑的请求参数。比如sleep(、union select、information_schema这类高频特征出现就拒绝访问再配合日志记录攻击来源。在 Spring Boot 项目里可以写一个简单的 Filter 或使用拦截器实现Component public class SqlInjectFilter implements Filter { private static final ListString DANGEROUS_KEYWORDS Arrays.asList( sleep(, benchmark(, union select, information_schema, extractvalue(, updatexml(, load_file( ); Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) request; MapString, String[] params httpRequest.getParameterMap(); for (String[] values : params.values()) { for (String value : values) { if (value ! null DANGEROUS_KEYWORDS.stream().anyMatch(value.toLowerCase()::contains)) { ((HttpServletResponse) response).setStatus(403); return; } } } chain.doFilter(request, response); } }不过这里有一个度的问题SQL 关键字检测很容易误伤正常业务。比如某个搜索接口的合法参数本身就叫select那这个过滤器就会把正常流量拦掉。所以网关层过滤只能作为临时手段不能作为长期依赖。4.2 应用层收窄关闭插件或限制租户来源如果多租户功能在某个模块暂时用不到最直接的方法是降低这个模块的风险等级。比如在不需要租户隔离的内部接口上临时不加入多租户插件或者把租户 ID 的来源全部改为服务端解析不再信任请求参数里的任何租户标识。另外一个兜底思路是写一个自定义的TenantLineHandler在返回租户 ID 之前做严格的校验public class SafeTenantLineHandler implements TenantLineHandler { Override public Expression getTenantId() { Long tenantId TenantContext.getTenantId(); if (tenantId null || tenantId 0) { throw new SecurityException(非法租户ID); } return new LongValue(tenantId); } }这样至少能避免攻击者通过伪造租户 ID 来访问其他租户数据。它防不了 SQL 解析层面的绕过但可以压缩攻击范围。4.3 数据库权限缩权与审计如果最坏的情况下漏洞被利用了那就要让攻击者的战果尽可能小。最直接的手段是数据库账号权限收窄。生产环境里不少应用连接数据库用的账号都带 DDL 权限这其实是很大的隐患。你可以做这样一个梳理权限建议SELECT / INSERT / UPDATE / DELETE保留业务正常运行的必要权限CREATE / DROP / ALTER / TRUNCATE从应用账号中回收由 DBA 单独执行超级管理员权限严禁配置到应用连接串中报表与分析账号单独创建只读账号不共用业务账号同时开启数据库审计日志重点关注异常时段的慢查询和不常见错误日志。出现可疑记录时至少能还原攻击者的操作痕迹。5. 这次漏洞给的安全基线反思把 SQL 安全重新捋一遍处理完升级和临时兜底这件事不应该到此为止。CVE-2023-25330 是一个适合用来做SQL 安全基线检查的契机。我把项目里的 SQL 写法、依赖管理和多租户测试用例全部盘点了一遍下面这几件事建议你也做一下。5.1 代码审计重点${} 拼接、IN 子句、ORDER BY最核心的审计目标是把所有使用${}的地方全部揪出来。IDEA 里直接全局搜索${}能搜到大部分但还有两种容易被漏掉的情况一种是 Java 代码里用String.format拼接 SQL 后再传给 Mapper另一种是 XML 里通过${}动态传入表名或排序字段。对于排序字段最安全的做法是白名单校验private static final ListString SORT_COLUMNS Arrays.asList(create_time, id, status); public String resolveSortColumn(String input) { if (!SORT_COLUMNS.contains(input)) { return id; } return input; }对于 LIKE 查询尽量使用参数绑定select idselectByKeyword resultType... SELECT * FROM orders WHERE title LIKE CONCAT(%, #{keyword}, %) /select对于 IN 子句不要用${ids}直接拼而是用 MyBatis 的foreach标签或者数据库的数组类型。5.2 把依赖漏洞扫描加进日常流程这次的 CVE 是依赖扫描工具发现的不是人工发现的。这本身就能说明问题人工周期性翻仓库公告的方式根本跟不上开源组件的发布节奏。你可以把 OWASP Dependency-Check 或者同类工具接入 CI 流程每次构建都跑一次一旦依赖列表里出现已知 CVE就让流水线失败。接入成本其实不高一个 Maven 插件就能完成plugin groupIdorg.owasp/groupId artifactIddependency-check-maven/artifactId version8.4.3/version executions execution goals goalcheck/goal /goals /execution /executions /plugin如果你们团队的发布频率很高也可以做成每个迭代手动跑一次关键是让它成为固定动作而不是想起来才检查。5.3 多租户数据隔离的测试用例设计升级之后一定要有一组针对多租户场景的回归用例。常见的覆盖点可以参考场景预期普通 SELECT 自动追加租户条件同一系统不同租户的数据互不可见UPDATE / DELETE 带条件执行不会误更新或误删其他租户数据子查询场景子查询中的业务表同样追加租户条件JOIN 场景连接的主表和关联表都追加租户条件分页插件 多租户插件组合分页总数和页数据均基于当前租户过滤INSERT 场景插入数据自动填充租户 ID测试方法不复杂写一个集成测试分别用两个租户 ID 访问同一接口断言返回结果中不包含对方租户的数据即可。重点是这些用例要变成自动化而不是只在升级时手动点一遍。6. 升级实操中我踩过的几个坑提前帮你们避一避升级本身不算复杂但实际操作过程中遇到的问题往往比升级动作本身更消耗时间。我把自己这次升级过程中遇到的和朋友反馈过的典型问题整理一下希望你们不要重走一遍。6.1 多租户插件和分页插件的执行顺序问题这是最容易踩的一个坑而且很有迷惑性。在MybatisPlusInterceptor的插件链里多个 inner interceptor 是有执行顺序的。正确做法是先添加TenantLineInnerInterceptor再添加PaginationInnerInterceptorMybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantLineHandler)); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));这样在分页插件生成 count 语句时SQL 已经被多租户插件改写过了count 语句里天然带上租户条件。如果顺序搞反分页插件先处理 SQL生成的总数语句可能没有租户条件就会出现总数对不上、甚至跨租户统计数据的问题。我见过一个项目升级后突然出现分页数量错误排查了半天才发现是某个同事重构时把两个插件的顺序写反了。这个问题的隐蔽之处在于单条数据查询看起来没问题只有数据量变多、分页翻页之后才暴露出异常。6.2 升级后出现 JSQLParser 解析异常的处理思路升级到 3.5.3.2 后jsqlparser 的解析能力增强了但同时也可能带来一个副作用以前能解析的 SQL在新版本解析器里反而报错。典型的表现是控制台输出类似这样的信息Caused by: net.sf.jsqlparser.parser.ParseException: Encountered unexpected token: FOR FOR这类报错多发生在使用了特殊写法的场景比如FOR UPDATE与分页插件组合、INSERT ... ON DUPLICATE KEY UPDATE、数据库特有的注释语法等。遇到这类问题我的排查顺序是先把报错的 SQL 完整打印出来然后逐步简化定位问题片段最后改写 SQL 为标准写法。有一种情况需要特别小心如果报错发生在租户条件改写的 SQL 上说明你的 SQL 本身不规范能改写尽量改写而不是绕过校验。真正规范的 SQL 在 3.5.3.2 上应当正常解析执行。6.3 强烈建议把租户隔离验证做成自动化回归用例我在这轮漏洞处理里做得最有价值的一件事是把多租户隔离的验证转型成自动化集成测试。之前都是人工在页面里切换账号验证又慢又不可靠。现在团队里每个涉及多租户的核心接口都配了一个测试用例用租户 A 创建数据再用租户 B 访问断言拿不到对方数据。这样以后不管升级 MybatisPlus、调整数据库配置还是有人不小心改了 SQL都能第一时间被测试拦住。安全问题的确需要靠制度和意识但落到工程层面最终还是要靠自动化用例来兜底因为人总会出错而用例不会。我的经验是像这种已经公开且有了修复版本的组件级漏洞处理节奏应该是当天评估影响面、两天内准备升级方案、一个迭代周期内完成灰度发布。不要拖拖得越久出问题的概率就越大。升级之后顺手把依赖扫描和租户隔离回归用例做起来这样才能让这次修复真正带来长期价值。