## user-progress 缺归属校验 学生端那条 GET /problem-sets/:id/user-progress 只有 requireTeacher,没有归属校验, 任何 Teacher Admin 都能读到别人建的题单的学生名单与进度。补上,口径与后台 loadOwned 一致(超管放行,其余人只能看自己建的),越权报 404。 顺带订正 docs/specs/phase4-review-authz.md:449:那条写着题单进度「显式下发真名」, 与代码不符 —— 学生端这条走 sampleUser 且没传 includeRealName,realName 恒为 null (SQL 里那次 leftJoin userProfile 是白查的)。真正下发真名的是后台那条 GET /admin/problem-sets/:id/progress,结论仍成立,但当时漏掉了归属校验这个缺口。 ## 头部进度的分母 分母只算必做题之后,ProblemSetHeader 还在拿 completedCount / problemsCount 算 —— 前者是必做完成数,后者是总题数。做完全部必做题的人会看到「9 / 10、90%」,而同一张 卡片上又标着「已完成」,题单 6 那 10 个人正是这种。改成读 userProgress,另外把 「另有 N 道选做」标出来,否则「共 10 道题目」和「9 / 9」对不上。 ## 截止时间 end_time 管的不是「到点不能做了」,是「到点之前看不到自己加入题单之前的旧代码」, 而学生端一个字都不显示 —— 被挡住的人不知道为什么,也不知道什么时候解锁。头部加一个 带解释的「截止 …」标签;提交列表那个锁图标的说明也补上另外两条解锁路径。 ## 两个必然筛空的筛选器 学生端题单列表的难度、状态两个下拉:线上 16 个题单全是 Easy / active,选「中等」 「困难」「已归档」永远是空列表。撤掉,保留关键词搜索。接口那两个 query 参数留着, 哪天真的用起这两个字段再把 select 加回来。 ## 题目移出题单时的提交记录 旧栈 problemset/signals.py 的 post_delete 会清掉该题在本题单的 ProblemSetSubmission, OJ2 没做,于是那张表一直在攒指向已移出题单的孤儿行。补上。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QqqZwxtXLo2GTqMi51C94D
对照表原写 DashboardInfoAPI / RandomUsernameAPI「旧后端任何人可读,含班级用户名 枚举」——不成立。两条都在 /api/admin/ 下,AdminRoleRequiredMiddleware (account/middleware.py:36)在中间件层就要求登录且 is_admin_role()。 「无装饰器」是真的,「任何人可读」不是。 真实缺口只是「任何管理员可读,而非仅超管」,严重度差很多。 修旧后端时按这条去查,会白改一处不存在的漏洞。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## 权限边界评审(后台 86 个 handler) 守卫本身一个没漏,问题全在对象级归属校验: - C1 跨题单删奖章:先按 (id, problemsetId) 校验奖章归属再删, 否则拿自己的题单 id + 别人的奖章 id 就能把别人的 user_badge 删掉 - C2 make-public 无归属校验:补 canEdit,越权者拿不到题面 - I1 两个分析端点被 `/problems/:id` 遮蔽 —— Hono 按注册顺序匹配, 不是静态优先。挪到 `/problem-analytics/*` - I2 from-public 只校验目标比赛归属:源题也必须是公开题库题 - I3 克隆比赛回传原比赛明文密码:克隆一律 password: null - I4 upload-image 守卫比旧后端严,教师写题面会 403:收回 requireAdmin 两个互不可见的教师账号实跑复验,六条全部拦住。 ## SQL 判题沙箱评审 - I-1 查询题只读被一句 `PRAGMA query_only=0` 关掉,实测 DML 拿到 AC。 query_only 自己就是个 PRAGMA,旧实现靠 authorizer 把 SQLITE_PRAGMA 一律拒了才没这个洞。现在 runStudent 逐语句拦 PRAGMA(用 sqlite3_normalized_sql 判关键字,注释和大小写由 SQLite 抹平), 并在每条语句前重放 query_only 和 max_page_count 兜底。 顺带修掉 M-1 里 max_page_count 学生可自行调大的部分。 - I-2 单条语句进了 step() 就打断不了,只能等父进程 SIGKILL, 而兜底时限是整个作业一口价 25s —— 1s 限的题要 26s 才判 TLE, 判题池只有 2 个槽,几发死循环就能把所有人堵住。 改成分阶段:子进程用 stderr 报 prepare/student/display, 父进程边读边换表,一进学生 SQL 就把兜底收到「题目时限 + 2s」。 实测 26s → 3.06s。 归因也跟着修了:卡在受信脚本(出题人的初始化脚本、标准答案) 现在报 SYSTEM_ERROR,不再当成学生超时甩 TLE。 engine.ts 头部那张防护对照表按实测重写 —— 原来那版把 query_only 写成等价于 authorizer 白名单,是不成立的。另记一笔:stock sql.js 的 wasm 没导出 progress_handler / interrupt / set_authorizer / limit, 想要得自己编,别再去翻了。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>