KEC 管理器 — 审查修复实施总结(2026-07-13)
依据
docs/安全与逻辑审查报告-2026-07-13.md的修复方案逐项落地。 结论:实际改动 11 处文件,其中 3 项(S3/S2/S7)经核实在当前代码中已被先前提交修复,本次仅验证确认、未做空操作;2 项(B4、S4)按安全优先原则做了有意的偏差处理(见下)。 验证:改动后端文件node --check全通过;运行相关测试 241 个全绿(arrange/statistics/semester/settings-reset 93 + middleware/auth/settings 148);B1 合班去重逻辑用真实dedupeTeachingUnits单元验证通过(旧 14 → 新 6)。
一、已落地修复(代码已改)
| # | 域 | 严重度 | 文件 | 改动要点 |
|---|---|---|---|---|
| B1 | 业务/课时 | High | server/src/services/arrange/queries.js | getTeachersForCourse 改用 dedupeTeachingUnits 去重后再聚合教师周课时,消除合班 N 倍虚高;auto-arrange 的 effectiveTotal 经同一返回值自动对齐。验证:合班场景 totalWeeklyHours 14→6,班级数不变 |
| B2 | 业务/筛选 | Medium | server/src/services/arrange/queries.js | 年级筛选循环内补 calc.grade !== Number(filters.grade) 精确复核,消除 over-inclusion(结果变少且正确) |
| B5 | 业务/课时 | Low | server/src/services/arrange/queries.js | weeksCount 增加 ?? 18 兜底,消除 weekly_hours * null = NaN |
| S1 | 安全 | Medium | server/src/app.js、server/src/routes/auth.routes.js | 为 apiLimiter 与 loginLimiter 增加 keyGenerator:取 X-Forwarded-For 链末尾真实客户端 IP,纵深防御伪造 XFF 绕过限流(权威修复仍依赖 Nginx proxy_set_header X-Forwarded-For $remote_addr;) |
| S5 | 安全 | Low | server/src/controllers/settings.controller.js | 系统重置不再 deleteMany() 整个 token_blacklist,改为只删 expires_at < now 的条目,避免已吊销令牌复效 |
| B3 | 业务/联动 | Low | server/src/controllers/plan/plan-matrix.controller.js | 更新/删除方案课程时,计算落在窗口外的 teaching_assignments 并在响应返回 danglingAssignments 提示(默认仅提示、不静默删除,避免误删仍有效历史安排) |
| B6 | 业务/注释 | Info | server/src/controllers/course.controller.js | 更正 deleteCourse 注释:DB 层 onDelete 实为 Restrict(非 Cascade),与 schema.prisma:320-322 一致 |
| F1 | 前端/约定 | Low | client/src/views/class/ClassList.vue | 班级批量更新 updates 键由 snake 改为 camelCase(majorId/collegeId/trainingLevelId/enrollmentYear/durationYears/isLeftSchool),与命名中间件约定及 ClassInput 对齐 |
| F2 | 前端/死接口 | Info | server/src/routes/auth.routes.js | 删除孤儿端点 POST /auth/download-token(前端从未调用,减少攻击面;AuthService.generateDownloadToken 保留供测试) |
| F3 | 前端/类型 | Info | client/src/api/types.js、client/src/api/audit.js | 新增 AuditLogResponse { logs, total, page, pageSize } 并修正 getAuditLogs 返回类型(原 PaginatedResponse.items 与实际 logs 不符) |
二、经核实已修复(本次未改,仅确认)
| # | 项 | 核实结论 |
|---|---|---|
| S3 | 导出文件名编码 | 全部 7 处 Content-Disposition 均已使用 encodeURIComponent(filename) + filename*=UTF-8''(82/151/328/491/575/784/918 行),报告中指出的「未编码」在当前代码已不存在 |
| S2 | XSRF Cookie 非 HttpOnly | 根因("Bearer 不落 localStorage")已实现:stores/auth.js:19 token 仅存 Pinia 内存;后端 token 为 httpOnly Cookie;sameSite:strict + Bearer 头不跨站自动携带。刻意加 httpOnly 到 XSRF-TOKEN 会破坏登录,未改(与报告警告一致) |
| S7 | 响应未输出清洗 | 前端模板无 v-html;useImport.js:10-14 已有 escapeHtml 且对 previewErrors 做了转义;后端 xss.js 已做输入清洗。XSS 前置条件已基本消除 |
三、有意偏差处理(未机械执行,附理由)
| # | 原计划 | 实际处理 | 理由 |
|---|---|---|---|
| B4 | 更新校验强制 duration_years 必填 | 未强制。创建已强制(validateClass);更新保持 .optional().isInt();建议对历史 null 行做一次性数据回填 | 若更新强制必填,批量改专业/学院等局部编辑(不含 duration_years)将 422 失败,属回归。F1 的批量更新也只在该类型才带该字段 |
| S4 | 下载令牌绑定资源/单次使用 | 未改(仅 F2 删除生成端点)。中间件 download_token 全局接受逻辑保留 | 删除生成端点后该路径已无令牌可生成(死代码),但为避免影响现有导出鉴权(Authorization + 查询令牌双路),未同步收窄中间件。建议后续作为独立改造:导出改为资源绑定签名 URL |
| S2 方案② | CSRF 改为服务端会话绑定 | 未做 | 需引入会话存储/Redis,属中大型改造,且当前双提交 + sameSite + Bearer 头已充分缓解跨站 CSRF,列入规划项而非本次 |
四、对各调用方的影响
- B1:排课界面教师"已排课时"、超限告警、自动排课容量与 dashboard/导出对齐(数字不再打架);合班场景下教师已排课时下降、告警更准、可排更多——属预期修正。已确认
auto-arrange.js:1060复用getTeachersForCourse返回值,effectiveTotal自动受益。 - B2:仅"按年级筛选"结果集(排课查询 + 含 grade 的导出)变少且正确;无 grade 筛选路径不受影响。
- B3:默认仅新增响应提示字段,向后兼容;不删除数据。
- S1:限流归因更准;同代理后多用户可能共享计数器(轻微),无功能破坏。
- S5:重置后旧会话更快失效(增强),无破坏。
- F1/F2/F3:纯维护性/类型准确性,无运行期行为变化。
五、验证记录
node --check6 个改动后端文件:全部 OK- ESLint 6 文件:0 error(4 个预存 unused-var warning,均非本次引入)
- 单元/集成测试:
vitest run相关 241 用例全通过- arrange/statistics/semester/settings-reset:93 passed
- middleware(auth/csrf/xss/validation/naming)/user/settings:148 passed
- B1 逻辑单测(真实
dedupeTeachingUnits):合班场景totalWeeklyHours旧 14 → 新 6,courseHours4,courseClassCount3 ✅
六、建议后续(非本次范围)
- 对
duration_years IS NULL的历史班级做一次数据回填(B4 根因)。 - S4 导出下载令牌改造为资源绑定签名 URL(缩短暴露面、单次使用)。
- S2 方案② CSRF 会话绑定(中大型改造,需排期)。
- B3 若需"一键清理悬空安排",建议新增独立管理员确认接口(本次仅提示)。