跳转到内容

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业务/课时Highserver/src/services/arrange/queries.jsgetTeachersForCourse 改用 dedupeTeachingUnits 去重后再聚合教师周课时,消除合班 N 倍虚高;auto-arrangeeffectiveTotal 经同一返回值自动对齐。验证:合班场景 totalWeeklyHours 14→6,班级数不变
B2业务/筛选Mediumserver/src/services/arrange/queries.js年级筛选循环内补 calc.grade !== Number(filters.grade) 精确复核,消除 over-inclusion(结果变少且正确)
B5业务/课时Lowserver/src/services/arrange/queries.jsweeksCount 增加 ?? 18 兜底,消除 weekly_hours * null = NaN
S1安全Mediumserver/src/app.jsserver/src/routes/auth.routes.jsapiLimiterloginLimiter 增加 keyGenerator:取 X-Forwarded-For 链末尾真实客户端 IP,纵深防御伪造 XFF 绕过限流(权威修复仍依赖 Nginx proxy_set_header X-Forwarded-For $remote_addr;
S5安全Lowserver/src/controllers/settings.controller.js系统重置不再 deleteMany() 整个 token_blacklist,改为只删 expires_at < now 的条目,避免已吊销令牌复效
B3业务/联动Lowserver/src/controllers/plan/plan-matrix.controller.js更新/删除方案课程时,计算落在窗口外的 teaching_assignments 并在响应返回 danglingAssignments 提示(默认仅提示、不静默删除,避免误删仍有效历史安排)
B6业务/注释Infoserver/src/controllers/course.controller.js更正 deleteCourse 注释:DB 层 onDelete 实为 Restrict(非 Cascade),与 schema.prisma:320-322 一致
F1前端/约定Lowclient/src/views/class/ClassList.vue班级批量更新 updates 键由 snake 改为 camelCase(majorId/collegeId/trainingLevelId/enrollmentYear/durationYears/isLeftSchool),与命名中间件约定及 ClassInput 对齐
F2前端/死接口Infoserver/src/routes/auth.routes.js删除孤儿端点 POST /auth/download-token(前端从未调用,减少攻击面;AuthService.generateDownloadToken 保留供测试)
F3前端/类型Infoclient/src/api/types.jsclient/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 行),报告中指出的「未编码」在当前代码已不存在
S2XSRF Cookie 非 HttpOnly根因("Bearer 不落 localStorage")已实现stores/auth.js:19 token 仅存 Pinia 内存;后端 token 为 httpOnly Cookie;sameSite:strict + Bearer 头不跨站自动携带。刻意加 httpOnlyXSRF-TOKEN 会破坏登录,未改(与报告警告一致)
S7响应未输出清洗前端模板无 v-htmluseImport.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 --check 6 个改动后端文件:全部 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,courseHours 4,courseClassCount 3 ✅

六、建议后续(非本次范围)

  1. duration_years IS NULL 的历史班级做一次数据回填(B4 根因)。
  2. S4 导出下载令牌改造为资源绑定签名 URL(缩短暴露面、单次使用)。
  3. S2 方案② CSRF 会话绑定(中大型改造,需排期)。
  4. B3 若需"一键清理悬空安排",建议新增独立管理员确认接口(本次仅提示)。