xzs-mysql-安全审计报告

15次阅读
没有评论

学之思开源考试系统(xzs 3.9.0 · MySQL 版)安全审计报告

项目 内容
审计对象 学之思开源考试系统(xzs),MySQL 分支,版本 3.9.0
仓库路径 /Users/h/Documents/02-tools/11-code/xzs-mysql
审计日期 2026-08-13
审计目标 反序列化、命令执行/RCE、SQL 注入、未授权访问、硬编码(含凭据)、XSS、SSRF、文件下载/上传越权等可获取系统/用户/应用权限或数据的问题
审计方式 人工代码审计 + 静态检索 + 字节码级验证 + PoC 实际验证(非黑盒,仅代码库)
结果统计 严重 2 · 高危 5 · 中危 8 · 低危 3;另附"已核查无问题项"清单

⚠️ 本报告基于代码仓库本身(含 application.yml、sql/*.sql、docker/* 中的真实配置与密钥)审计所得。报告中所有密钥、密码、token 均直接来自仓库文件,请按"修复建议"章节立即轮换/整改。


一、技术栈与攻击面概况

  • 后端: Spring Boot 2.1.6(2019 年)、Spring Security 5.1.5、MyBatis + PageHelper、Undertow、七牛云 SDK、RSA(1024bit, PKCS1)
  • 前端: Vue 2(xzs-admin 后台 / xzs-student 学生端)、微信小程序原生(wx/xzs-student)
  • 数据库: MySQL 8.0.17
  • 认证: Spring Security(ADMIN/STUDENT 角色)+ Remember-Me(TokenBasedRememberMeServices)+ 微信 OpenId token 拦截器(TokenHandlerInterceptor)
  • 主要攻击面:
    1. /api/admin/** —— 后台管理接口
    2. /api/student/** —— 学生端接口
    3. /api/wx/** —— 微信小程序接口(未纳入 Spring Security,由 token 拦截器保护,且存在 ignore 白名单)
    4. 富文本试题内容 —— UEditor → 后端零消毒 → 前端 v-html 渲染
    5. 配置文件 —— application*.yml、sql/*.sql、docker/* 中存在生产密钥与明文口令

二、漏洞汇总

# 严重度 类别 漏洞 关键位置 影响
V1 🔴 严重 硬编码密钥 + 可逆密码 RSA 私钥硬编码,密码可逆存储 application.yml、AuthenticationServiceImpl.java:52-58 数据库泄露 → 全量密码明文,可登录任意账号
V2 🔴 严重 认证绕过 / Cookie 伪造 Remember-Me Cookie 可离线伪造(密钥硬编码 "xzs") CookieConfig.java、SecurityConfigurer.java:93 免密获得 30 天管理员会话
V3 🟠 高危 越权 / 敏感数据泄露 开考前泄露任意试卷标准答案 ExamPaperController.java:36、QuestionServiceImpl.java:105-140 学生可枚举试卷 id 获取答案
V4 🟠 高危 IDOR 越权读取 答卷越权读取(web + 微信) ExamPaperAnswerController.java:107(student)、wx/.../ExamPaperAnswerController.java:125 读取任意考生答卷
V5 🟠 高危 IDOR 越权写 越权批改/篡改成绩 ExamPaperAnswerController.java:86、ExamPaperAnswerServiceImpl.java:112-130 任意改分、覆盖他人答卷
V6 🟠 高危 IDOR 越权读取 题目作答记录越权读取 QuestionAnswerController.java:62 读取任意考生作答记录
V7 🟠 高危 存储型 XSS 试题/答案富文本零消毒 + v-html 渲染 后端 HtmlUtil.java、前端 Show.vue / QuestionAnswerShow.vue 管理端/全体学生 XSS,可窃会话、改密、钓鱼
V8 🟡 中危 未授权访问 文件上传接口免认证且无类型校验 application.yml(ignore-urls)、UploadController.java:44 未授权滥用七牛存储、恶意文件托管
V9 🟡 中危 CORS 配置错误 allowedOrigins("*") + allowCredentials(true) SecurityConfigurer.java:104-115 任意站点携带凭证跨域请求 API
V10 🟡 中危 硬编码凭据 七牛 access/secret key、微信 appid 硬编码 application.yml 云存储被接管、流量被滥用
V11 🟡 中危 硬编码凭据 数据库 root/123456 + allowMultiQueries + 无 SSL application-prod.yml 等、docker-compose.yml 数据库弱口令 + 放宽 JDBC 限制
V12 🟡 中危 安全配置 登录无防爆破、默认账号固定 SecurityConfigurer.java、sql/xzs-mysql.sql:253 口令暴力破解
V13 🟡 中危 安全配置 .anyRequest().permitAll() 兜底放行 SecurityConfigurer.java:89 新接口默认未授权可访问
V14 🟡 中危 业务逻辑 时段试卷无时间窗口校验 ExamPaperAnswerServiceImpl.java 非考试时段提交答卷
V15 🟡 中危 业务逻辑 任务试卷无年级/可见性校验 ExamPaperServiceImpl.java:116 跨年级作答任务试卷
V16 🟡 中危 明文传输 微信端 baseAPI 明文 HTTP wx/xzs-student/app.js:8 链路窃听/token 泄露
V17 🟢 低危 DoS / 健壮性 无扩展名文件名 500;Integer.parseInt 裸解析 500 UploadController.java:57、wx/.../ExamPaperAnswerController.java:93 异常参数可致 500
V18 🟢 低危 供应链 依赖/组件版本过旧(2019) pom.xml、前端 package.json 大量已公开 CVE 无补丁
V19 🟢 低危 会话安全 无 SameSite、csrf 全局禁用 SecurityConfigurer.java:94 CSRF 相关风险放大

三、漏洞详情

V1 🔴 硬编码 RSA 私钥 + 密码可逆存储(严重)

位置:

  • source/xzs/src/main/resources/application.yml → system.pwdKey.privateKey / publicKey(1024 位 RSA 私钥硬编码在仓库中)
  • source/xzs/src/main/java/com/mindskip/xzs/service/impl/AuthenticationServiceImpl.java:52-58
  • source/xzs/src/main/java/com/mindskip/xzs/utility/RsaUtil.java:42-74

代码:

// AuthenticationServiceImpl
public String pwdEncode(String password) {                 // L52
    return RsaUtil.rsaEncode(systemConfig.getPwdKey().getPublicKey(), password);
}
public String pwdDecode(String encodePwd) {                // L57
    return RsaUtil.rsaDecode(systemConfig.getPwdKey().getPrivateKey(), encodePwd);
}
// authUser (L39-48): 把库里密文解密成明文后与输入比较
String pwd = pwdDecode(encodePwd);
return pwd.equals(password);
// RsaUtil —— 默认 "RSA" = ECB/PKCS1Padding
final Cipher cipher = Cipher.getInstance(ALGORITHM);       // L45
cipher.init(Cipher.DECRYPT_MODE, priKey);                  // L74

问题: 登录验证是把存储的"密文"用硬编码在仓库里的私钥解密回明文再 equals。密码本身是可逆存储,且私钥公开:

  • 拿到数据库(或仓库自带的 sql/xzs-mysql.sql)即拿到全量用户密码明文,无需破解;
  • 私钥泄露使整个密码体系形同虚设。

✅ 已实际验证(见"五、验证证据"):用仓库硬编码私钥解密 sql/xzs-mysql.sql 中默认 admin 密码密文 → 123456。

修复建议:

  1. 停用 RSA 可逆存储,改用不可逆哈希:BCrypt(或 scrypt/argon2),现有密码一次性迁移(登录时解密→验明→重哈希)。
  2. 私钥/密钥从配置文件中移除,放入密钥管理系统(Vault/KMS)或环境变量,并立即轮换。
  3. 废除 pwdDecode 全链路;任何"能解出明文密码"的能力都不应存在。

V2 🔴 Remember-Me Cookie 离线伪造 → 30 天管理员会话(严重)

位置:

  • source/xzs/src/main/java/com/mindskip/xzs/configuration/spring/security/SecurityConfigurer.java:93
  • source/xzs/src/main/java/com/mindskip/xzs/configuration/property/CookieConfig.java(getName() 返回 "xzs",getInterval() 返回 30 天)
  • 默认管理员口令:sql/xzs-mysql.sql:253

代码:

.and().rememberMe()
     .key(CookieConfig.getName())                      // 签名密钥 = "xzs" (硬编码)
     .tokenValiditySeconds(CookieConfig.getInterval()) // 30 天
     .userDetailsService(formDetailsService)

问题: 使用 TokenBasedRememberMeServices 的默认行为。cookie 签名密钥是硬编码字符串 "xzs";而签名输入需要 password,该值通过 V1 的 RSA 解密即可获得。攻击者具备 密钥 + 用户名 + 密码明文 三要素即可离线伪造任意用户的 remember-me cookie。

伪造格式(通过对 Spring Security 5.1.5 字节码反编译确认,详见附录 A):

expiry(ms)  = System.currentTimeMillis() + 1000 * tokenValiditySeconds(毫秒)
signature   = hex( MD5( username + ":" + expiry + ":" + password + ":" + key ) )   // 32 位小写 hex
cookie      = base64( URLEncode(username) + ":" + expiry + ":" + signature )        // 去掉末尾 "=" 填充

✅ 已实际伪造:利用 V1 解出的 admin/123456 + 硬编码 key xzs,伪造出有效期为 2026-09-12(30 天)的 admin remember-me cookie:

YWRtaW46MTc4OTE4NjkwMDc0NzoyYmRjZjRiMmU2NTgyZDNjYTBhMjY3YzM1ZGY4Mjg0NQ

发送 Cookie: remember-me=YWRtaW46MTc4OTE4NjkwMDc0NzoyYmRjZjRiMmU2NTgyZDNjYTBhMjY3YzM1ZGY4Mjg0NQ 即可免密以 ADMIN 身份调用全部 /api/admin/** 接口(越权、读用户、改数据、上传等)。

前提链: 需要数据库密码密文(SQL 备份即公开)+ 仓库内私钥 + 仓库内密钥。三者全部在仓库内,门槛≈拿到源码。

修复建议:

  1. 更换 key 为强随机长密钥,放环境变量/KMS,禁止入库。
  2. 改用 PersistentTokenRepository 持久化 token,并使 token 随机且服务端可吊销。
  3. 升级依赖(5.1.x 的 remember-me 设计即存在此风险面),并建议对 admin 启用二次认证。
  4. 限制登录失败次数 + 更换默认口令。

V3 🟠 开考前泄露任意试卷标准答案(高危)

位置:

  • source/xzs/src/main/java/com/mindskip/xzs/controller/student/ExamPaperController.java:36-37
  • source/xzs/src/main/java/com/mindskip/xzs/service/impl/ExamPaperServiceImpl.java:116-145
  • source/xzs/src/main/java/com/mindskip/xzs/service/impl/QuestionServiceImpl.java:105-140
  • 注册接口免认证:/api/student/user/register(UserController.java:58,在 ignore-urls 中)

代码:

// ExamPaperController (student)
@RequestMapping(value = "/select/{id}", method = RequestMethod.POST)   // L36
public RestResponse<ExamPaperEditRequestVM> select(@PathVariable Integer id) { // L37
    ExamPaperEditRequestVM vm = examPaperService.examPaperToVM(id);
    ...
}
// ExamPaperServiceImpl.examPaperToVM (L116) —— 无可见性/年级/时间段校验,直接复用编辑视图
for (ExamPaperQuestionItemVM q : ...) {
    QuestionEditRequestVM questionEditRequestVM = questionService.getQuestionEditRequestVM(question); // L131
    ...
}
// QuestionServiceImpl.getQuestionEditRequestVM —— 把答案字段塞进学生可见的返回体
questionEditRequestVM.setCorrect(question.getCorrect());             // L124
questionEditRequestVM.setCorrectArray(...);                          // L127
questionEditRequestVM.setAnalyze(questionObject.getAnalyze());       // L140

问题: 学生端"查看试卷"接口直接复用后台编辑视图,把每题的 correct(正确答案)/correctArray/analyze(解析)原样返回。且:

  • 无任何"试卷是否属于当前学生/是否开考/是否本年级"校验;
  • id 为自增整数,可枚举;
  • 配合免认证注册,任何人在考前即可拿全部答案。

影响: 考试数据完整性被彻底破坏;开考泄露答案。这也是 V15 的根因。

修复建议:

  1. examPaperToVM 拆成两套视图:学生作答版(剥离 correct/analyze)与批改回看版(仅本人、开考后)。
  2. select/{id} 增加归属/可见性/时间窗口校验。
  3. id 改 UUID 化,并做水平权限校验。

V4 🟠 答卷越权读取 IDOR(高危)

位置:

  • source/xzs/src/main/java/com/mindskip/xzs/controller/student/ExamPaperAnswerController.java:107-116
  • source/xzs/src/main/java/com/mindskip/xzs/controller/wx/student/ExamPaperAnswerController.java:125-134

代码(web 端):

@RequestMapping(value = "/read/{id}", method = RequestMethod.POST)   // L107
public RestResponse<ExamPaperReadVM> read(@PathVariable Integer id) { // L108
    ExamPaperAnswer examPaperAnswer = examPaperAnswerService.selectById(id); // 直接按 id 查,无 createUser 校验
    ExamPaperEditRequestVM paper = examPaperService.examPaperToVM(examPaperAnswer.getExamPaperId());
    ExamPaperSubmitVM answer = examPaperAnswerService.examPaperAnswerToVM(examPaperAnswer.getId());
    vm.setPaper(paper);   // 含 V3 的正确答案字段
    vm.setAnswer(answer); // 含该考生完整作答
    ...
}

微信端 read/{id}(wx/.../ExamPaperAnswerController.java:125)同款问题。

问题: id 自增可枚举,任何登录学生可遍历读取任意考生的答卷(含作答内容与得分),web 端还连带返回正确答案。属典型水平越权(IDOR)。

修复建议: read 前按 createUser == 当前用户 过滤;微信端同样处理;对答卷 id 做不可枚举化。


V5 🟠 越权批改 / 篡改成绩 IDOR(高危)

位置:

  • source/xzs/src/main/java/com/mindskip/xzs/controller/student/ExamPaperAnswerController.java:86-105
  • source/xzs/src/main/java/com/mindskip/xzs/service/impl/ExamPaperAnswerServiceImpl.java:112-166

代码:

// ExamPaperAnswerController.edit (L86)
@RequestMapping(value = "/edit", method = RequestMethod.POST)
public RestResponse edit(@RequestBody @Valid ExamPaperSubmitVM examPaperSubmitVM) {
    ...
    if (status != Complete) { ... }          // 仅判断"不是已完成",未校验归属
    String score = examPaperAnswerService.judge(examPaperSubmitVM); // L98
    ...
}
// ExamPaperAnswerServiceImpl.judge (L112-130)
public String judge(ExamPaperSubmitVM examPaperSubmitVM) {
    ExamPaperAnswer examPaperAnswer = examPaperAnswerMapper.selectByPrimaryKey(examPaperSubmitVM.getId()); // L113 直接按提交的 id 取记录,无 createUser 校验
    ...
    examPaperAnswer.setUserScore(customerScore);  // L130 学生可任意自定 score 字段
    ...
}

问题: 任何学生(甚至 ADMIN 外的普通账号)可提交 {id: 他人答卷id, answerItems:[...], score: 100} 来覆盖他人答卷、篡改任意考生成绩。配合 V3/V4 可读取正确答案后再改分,影响评分公信力。

修复建议:

  1. judge 入口按 createUser + 状态做严格鉴权(仅本人/管理员,且仅在批改状态下)。
  2. 服务端重算分数,拒绝客户端传 score(或对 score 做签名/服务端校验)。
  3. 增加批改操作审计日志。

V6 🟠 题目作答记录越权读取 IDOR(高危)

位置:

  • source/xzs/src/main/java/com/mindskip/xzs/controller/student/QuestionAnswerController.java:62-71

代码:

@RequestMapping(value = "/select/{id}", method = RequestMethod.POST)  // L62
public RestResponse<QuestionAnswerVM> select(@PathVariable Integer id) { // L63
    ExamPaperQuestionCustomerAnswer examPaperQuestionCustomerAnswer = examPaperQuestionCustomerAnswerService.selectById(id); // L65 无归属校验
    ...
}

问题: 可直接按 id 读取任意考生的单题作答记录,同 IDOR。

修复建议: 按当前用户 id 过滤;id 不可枚举。


V7 🟠 存储型 XSS(富文本零消毒 + v-html 渲染)(高危)

位置:

  • 后端: source/xzs/src/main/java/com/mindskip/xzs/utility/HtmlUtil.java(仅正则 clear() 去标签,非安全消毒器);试题/答案/题目内容以富文本存 TextContent,无 Jsoup/DOMPurify 等消毒
  • 前端后台: source/vue/xzs-admin/src/views/exam/question/components/Show.vue(第 4/8/13/17/22/25/30/33 行 v-html)
  • 前端学生端: source/vue/xzs-student/src/views/exam/components/QuestionAnswerShow.vue(第 5/10/16/21/27/31/37/45/67/71/72 行 v-html)、QuestionEdit.vue
  • 微信端: wx/xzs-student 中 rich-text 组件渲染同一后台富文本(nodes 含可点击事件,HTML 注入面)

问题:

  1. 题目内容由管理员在后台富文本编辑器(UEditor)录入,前端 v-html 直接渲染;插入 <img onerror=...>、<a href=javascript:...> 等即触发;
  2. 后端对富文本不消毒(HtmlUtil.clear() 只做粗糙去标签,会误删且不拦截事件属性);
  3. 攻击者(考生或入侵者)只要能把任意富文本写入任一展示给管理员的题目/答案,即可在管理员浏览器执行 JS,进而:发起改密请求、调用后台 API、窃取页面内数据、钓鱼。

影响面: 管理端 + 全体学生端,XSS 可达。

修复建议:

  1. 引入白名单式消毒(后端 OWASP Java HTML Sanitizer / Jsoup Cleaner,前端 DOMPurify),默认拒绝 on*/javascript:/data:。
  2. v-html 全部替换为消毒后的渲染;富文本字段统一走管道消毒。
  3. 微信 rich-text 仅渲染白名单标签。
  4. 生产使用 CSP,并将 session/remember-me cookie 保持 HttpOnly(已确认当前 HttpOnly 开启,故重点防"以受害者身份操作")。

V8 🟡 未授权文件上传(免认证 + 无类型校验)

位置:

  • source/xzs/src/main/resources/application.yml:security-ignore-urls 含 /api/admin/upload/configAndUpload、/api/admin/upload/auth
  • source/xzs/src/main/java/com/mindskip/xzs/controller/admin/UploadController.java:44-68
  • source/xzs/src/main/java/com/mindskip/xzs/service/impl/FileUploadImpl.java:33-47

代码:

@RequestMapping("/configAndUpload")                       // L44 —— 免认证
public RestResponse upload(...) {
    MultipartFile multipartFile = multipartHttpServletRequest.getFile(IMAGE_UPLOAD_FILE); // L50
    String imageType = imgName.substring(imgName.lastIndexOf("."));                       // L57 仅按扩展名
    String fileUrl = fileUpload.uploadFile(...);                                          // 直传七牛
}
// FileUploadImpl
Response response = uploadManager.put(inputStream, null, upToken, null, null); // key 由七牛自动生成

问题:

  1. 上传接口在安全白名单内,完全免认证,任何人可匿名上传;
  2. 仅按扩展名判断,无文件类型/魔数/MIME/内容校验;
  3. 上传至七牛对象存储,key 自动生成(无法直接落地服务器 webshell),但可被滥用为:恶意文件托管(C2/钓鱼/恶意 payload 分发)、存储与流量消耗(刷流)、配合 404 页/内容分发。若七牛桶权限配置不当,还可越权读/删桶内文件。

修复建议:

  1. 上传接口移出 ignore-urls,必须登录 + 授权;
  2. 增加白名单扩展名 + 魔数校验 + 大小限制 + 频控;
  3. 使用私有桶/签名下载,限制 MIME 与防盗链;上传日志审计。

V9 🟡 CORS * + allowCredentials(true)

位置: source/xzs/src/main/java/com/mindskip/xzs/configuration/spring/security/SecurityConfigurer.java:104-115

allowedOrigins("*")
.allowCredentials(true)

问题: 允许任意来源携带凭证跨域。配合 V4-V6 的 IDOR 与登录态,恶意站点可在受害者浏览器中静默调用本系统 API。浏览器规范不允许 *+credentials 同时生效(会被忽略),但配置本身即错误信号,且各中间件/历史版本行为不一,风险应修复而非依赖浏览器兜底。

修复建议: 改为明确的受信来源白名单;allowCredentials(true) 时禁止 *;生产走 HTTPS。


V10 🟡 硬编码云密钥(七牛 / 微信)

位置: source/xzs/src/main/resources/application.yml

qn:
  access-key: KabOBTPGVll2sSV8d1OIlW8G4_n_cReE7RSsLafA
  secret-key: mSltk9_9KtCdFetCmxvvCAgsBg8JZHiXMuQeqSJA
wx:
  appid: wxa0d64cf3de0cc0ce
  secret: ""          # 空 secret 本身即配置错误

问题: 七牛 secret-key 具备上传/管理对象存储权限,硬编码在仓库中等于公开;任何拿到源码的人可接管该七牛空间(上传/覆盖/读取/删除)。微信 appid 亦泄露且 secret 为空(会导致登录链路失效或依赖错误配置)。

修复建议: 立即吊销并在云控制台重置两组密钥;密钥改由环境变量/KMS 注入;空 secret 补全或移除微信登录。


V11 🟡 数据库弱口令 + JDBC 放宽

位置:

  • source/xzs/src/main/resources/application-prod.yml / dev / test / pre:
spring.datasource.url: jdbc:mysql://...:3306/...?allowMultiQueries=true&useSSL=false
spring.datasource.username: root
spring.datasource.password: 123456
  • docker/docker-compose.yml:MYSQL_ROOT_PASSWORD: "123456"

问题:

  1. root/123456 生产弱口令;
  2. allowMultiQueries=true 放宽 SQL 语义(即使目前无注入点,也为后续/供应链风险放大条件);
  3. useSSL=false 明文链路。

修复建议: 强口令 + 独立最小权限账号;去掉 allowMultiQueries;启用 TLS/SSL;连接串禁止入库。


V12 🟡 登录无防爆破 + 默认账号

位置: SecurityConfigurer.java(认证链)、sql/xzs-mysql.sql:253(默认 admin)

问题:

  • 登录接口无验证码/失败锁定/频率限制,可在线爆破;
  • 默认 admin 口令(经 V1 验证为 123456)在 SQL 备份中公开,若生产沿用默认口令等于裸奔。

修复建议: 引入验证码 + 登录失败锁定 + 审计;强制首登改密;禁止默认口令。


V13 🟡 .anyRequest().permitAll() 兜底放行

位置: SecurityConfigurer.java:89

.anyRequest().permitAll()

问题: 所有未显式匹配的路径默认放行,安全完全依赖前面几条 antMatcher 枚举的完整性。一旦漏配即未授权访问。建议反转为 .anyRequest().authenticated() 兜底。


V14 🟡 时段试卷无时间窗口校验

位置: ExamPaperAnswerServiceImpl.java(calculateExamPaperAnswer/answerSubmit 链路)

问题: 试卷可配置"考试时间段",但提交链路未校验当前时间是否在窗口内,学生可在任意时间提交时段试卷。

修复建议: 提交/读取时校验 paperTime 窗口与时长,服务端强校验。


V15 🟡 任务试卷无年级/可见性校验

位置: ExamPaperServiceImpl.java:116(examPaperToVM)、任务试卷列表/详情链路

问题: 学生可查看并作答非本年级/本班的任务试卷(与 V3 同源,复用编辑视图)。泄露面同 V3。

修复建议: 学生侧严格按年级/班级/时间过滤试卷列表与详情。


V16 🟡 微信端明文 HTTP

位置: source/wx/xzs-student/app.js:8

baseAPI: "http://localhost:8000"

问题: 小程序请求走明文 HTTP,登录 token 可被链路窃听。

修复建议: 改为 HTTPS 正式域名,且与微信后台 request 合法域名一致。


V17 🟢 低危 · DoS/健壮性

位置:

  • UploadController.java:57:imgName.substring(imgName.lastIndexOf(".")) —— 文件名无扩展名时 lastIndexOf 返回 -1 → substring(-1) 抛异常 500;
  • wx/student/ExamPaperAnswerController.java:93-94:Integer.parseInt(request.getParameter("id")) / doTime 裸解析,非数字参数抛 500(可被用来刷错误日志/轻微 DoS)。

修复建议: 判空/兜底、try-catch 或使用安全转换;统一异常处理返回 400。


V18 🟢 低危 · 依赖版本过旧

位置: source/xzs/pom.xml(Spring Boot 2.1.6, 2019)、前端 package.json、wx/*

问题: Spring Boot/Security 2.1.x/5.1.x、Undertow、jQuery 等均为 2019 版本,含大量已公开 CVE(如 Spring Security 相关、Undertow、前端库),长期无官方修复。

修复建议: 规划升级到受支持版本(Spring Boot 2.7+/3.x),升级前至少跟进安全公告并做最小补丁。


V19 🟢 低危 · 会话安全配置

位置: SecurityConfigurer.java:94(.csrf().disable());remember-me / session cookie 未显式设置 SameSite

问题: CSRF 全局关闭 + 无 SameSite,cookie 在跨站请求中默认携带,放大 V9 CORS 与各类伪造请求风险。

修复建议: 评估保留 CSRF 或改用同源校验 + SameSite=Lax/Strict + 额外 token;不降级安全默认值。


四、已核查无问题项(结论)

类别 结论 依据
SQL 注入 未发现 全部 MyBatis mapper 使用 #{} 预编译,未发现 ${} 拼接(抽查 UserMapper.xml 等);分页/排序参数均参数化
反序列化 未发现 使用 Spring Boot 默认 Jackson(未开启 defaultTyping/多态反序列化),未发现 Fastjson/XStream/自定义反序列化 gadget 入口;无 readObject 用户输入面
命令执行 / RCE 未发现 未发现 Runtime.exec/ProcessBuilder/JNDI lookup/SpEL·OGNL 表达式注入/模板注入等入口
SSRF 未发现 WxUtil.getOpenId 请求 URL 固定 https://api.weixin.qq.com/...,参数仅拼入 query;七牛 SDK 上传目标固定;无用户可控 URL 的请求点
路径穿越 / 任意文件读取 未发现 上传走七牛对象存储(本地不落盘);未发现基于用户输入拼路径的本地文件读写/下载接口
模板注入 / SpEL 未发现 无用户输入进入模板引擎或表达式上下文

注:以上基于代码静态审计与抽样核查,结论为该代码版本下未发现,不代表绝对安全;升级依赖后需重新复核反序列化/表达式注入面。


五、验证证据(实际演示)

证据 1:默认 admin 密码可逆 → 明文 123456

  1. sql/xzs-mysql.sql 第 253 行存储默认管理员 RSA 密文;
  2. 使用 application.yml 中硬编码私钥以 openssl pkeyutl/Java 解密 → 得到明文 123456;
  3. 印证 V1:密码为可逆存储 + 私钥公开。

证据 2:Remember-Me Cookie 离线伪造成功

伪造条件:admin + 123456(证据 1)+ 硬编码 key "xzs"。

算法(已通过 Spring Security 5.1.5 字节码逐条核验):

expiry(ms) = 1789186900747      # = now + 1000 * 2592000 (30 天)
signature  = hex(md5("admin:1789186900747:123456:xzs"))
           = 2bdcf4b2e6582d3ca0a267c35df82845
cookie     = base64("admin:1789186900747:2bdcf4b2e6582d3ca0a267c35df82845") 去掉末尾 "="

最终伪造出的 ADMIN 会话 Cookie(有效期至 2026-09-12):

YWRtaW46MTc4OTE4NjkwMDc0NzoyYmRjZjRiMmU2NTgyZDNjYTBhMjY3YzM1ZGY4Mjg0NQ

用法:

GET/POST /api/admin/user/current
Cookie: remember-me=YWRtaW46MTc4OTE4NjkwMDc0NzoyYmRjZjRiMmU2NTgyZDNjYTBhMjY3YzM1ZGY4Mjg0NQ

→ 直接以 admin(ADMIN 角色)身份通过 Spring Security 认证,获取全部后台数据与操作能力。

该值已用 Python 与 Java(逐条复刻 5.1.5 字节码逻辑)双实现交叉验证一致。

证据 3:答案泄露链路

匿名注册学生 (/api/student/user/register,免认证)
  └─> POST /api/student/exam/paper/select/{id}   (id 自增可枚举)
        └─> examPaperToVM(id)  ──> getQuestionEditRequestVM(question)
              └─> setCorrect / setCorrectArray / setAnalyze  → 返回体含标准答案

六、修复优先级路线图

优先级 动作 对应漏洞
P0(立即) 轮换七牛/微信/数据库全部密钥口令;密码改 BCrypt 一次性迁移;更换 remember-me key 并改持久化 token;吊销泄露凭据 V1 V2 V10 V11
P0(立即) anyRequest().authenticated() 兜底;上传接口移出白名单 + 类型校验 V13 V8
P1(短期) 修补 4 处 IDOR(答卷读/写、题目作答、试卷答案)与试卷可见性;judge 服务端重算分数 V3 V4 V5 V6 V15 V14
P1(短期) 富文本白名单消毒(后端 + 前端 DOMPurify),替换 v-html V7
P2(中期) CORS 白名单、CSRF/SameSite、防爆破、异常处理健壮性、HTTPS V9 V12 V16 V17 V19
P3(长期) 升级 Spring Boot/Security 与前端组件,重跑审计 V18

附录 A:Spring Security 5.1.5 Remember-Me 格式(反编译确认)

来源:spring-security-web-5.1.5.RELEASE.jar → TokenBasedRememberMeServices / AbstractRememberMeServices(字节码)。

// TokenBasedRememberMeServices.makeTokenSignature —— 签名用 HEX,非 Base64
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + key;
MessageDigest digest = MessageDigest.getInstance("MD5");
return new String(Hex.encode(digest.digest(data.getBytes())));   // 32 位小写 hex

// onLoginSuccess —— expiry 用毫秒
long expiryTime = System.currentTimeMillis() + 1000L * tokenValiditySeconds;
// tokens = {username, Long.toString(expiryTime), signature}

// AbstractRememberMeServices.encodeCookie
String joined = URLEncode(t0)+":"+URLEncode(t1)+":"+URLEncode(t2);
String cookie = Base64.getEncoder().encode(joined.getBytes());
while (cookie.endsWith("=")) cookie = cookie.substring(0, cookie.length()-1); // 去掉 '=' 填充

注:5.1.x 中 cookie 内签名是 hex,部分网文写成 base64 属旧版本或误传;本报告依据实际 jar 字节码核验。


报告完 · 审计范围:仓库源码、配置、SQL 初始化、Docker 编排、Vue/微信前端。所有复现均基于仓库自身数据,未触碰线上环境。

正文完
 0