fix(security): 修复上传名单文件可被 URL 直连访问(nginx 下可 getshell)

严重漏洞:roster_import 使用 $_FILES 的原始扩展名拼 roster_YYYYMMDD_HHMMSS.<ext>
落在 Web 根目录内的 data/uploads/,既无扩展名白名单(可上传 .php),文件名又
可按秒枚举;而 bootstrap 只写了 data/.htaccess,nginx 不读 .htaccess,
生产环境(nginx + PHP-FPM)下可直接远程代码执行。

修复(api.php roster_import 四层防护):
- 扩展名白名单:仅 xlsx / xlsm / csv / txt
- 随机文件名:bin2hex(random_bytes(16)),杜绝枚举
- 解析完成后 @unlink 立即删除,Web 目录下不留存任何上传文件
- 20MB 大小限制 + UPLOAD_ERR 检查

加固(bootstrap.php):data 及 cache/uploads/snapshots 每个目录都写
.htaccess(Deny from all)+ index.html 占位;README 与文档补充 nginx
location ^~ /data/ { deny all; return 404; } 规则。

测试:新增 e2e tools/test-upload-guard.js(11 断言,覆盖 .php/.phtml 被拒、
无残留、合法 csv 导入成功且即删、无可枚举文件名),并入 tests/run-all.sh。
全套 90 项通过。

docs: 新增 VitePress 文档站(18 页)
- 使用指南 12 页:简介/部署/快速上手/系统设置/名单管理/统计口径/试卷设置/
  快照趋势异常/导出打印/数据看板/随机抽取/常见问题
- 安全 2 页:安全总览(含本次漏洞复盘)与部署加固清单(P0-P2)
- 开发 4 页:架构与目录/接口一览/SQLite 存储/测试
- 配图迁移至 docs/public/images/;.gitignore 忽略文档构建产物
This commit is contained in:
2026-09-10 22:42:13 +08:00
parent 157f257728
commit a3be81b261
33 changed files with 4802 additions and 17 deletions
+67
View File
@@ -0,0 +1,67 @@
# 试卷设置(满分 / 合格率 / 免考)
不同试卷满分往往不同(100 分制、150 分制、50 道题每题 2 分……),合格率要求也可能不同。
所以**满分的每卷配置放在主页面「试卷设置」里,而不是系统设置**——设置里只保留全局默认值。
## 打开方式
主页面工具栏 → **试卷设置**(按钮为高亮主色)。配置**只对当前选中的试卷生效**。
## 优先级
```
本卷配置 > 全局默认(系统设置)
```
未设置某项时显示"跟随全局(X)",保存后卡片标题与统计口径立即更新。
## 满分
- 用于合格率阈值计算和卡片展示;
- 留空或填 `0` = **清除本卷配置**,回落全局默认;
- 全局默认满分在 **设置 → 全局默认满分** 中修改。
## 合格率
- 百分比,含义是「成绩 ≥ 满分 × X%」记合格。例如满分 150、合格率 60 → 90 分及格;
- 留空 = 跟随全局;录入范围会被夹到 `1 ~ 100`;
- 保存后统计卡片标题会变成 `合格率 (≥60%满分)`,与实际口径一致。
::: tip 为什么不再写死 60%
早期版本把「≥60% 满分」硬编码在代码里,一旦满分不是 100 就会产生误导。
现在阈值可配、可每卷覆盖,且界面上直接显示当前阈值。
:::
## 免考名单
某些人因故不参加本卷考试(缓考、免修、已单独补考等),如果直接把他们从名单里删除,
其它试卷的统计也会受影响。免考名单解决的就是这个:
- 填写**学号**列表,支持逗号、中文逗号、空格、换行分隔;
- 生效范围:仅当前试卷;
- 效果:这些人**不计入名单总人数,也不计入缺考**,彻底从本卷的分母中移除;
- 若他们实际上也交了卷,答卷仍会出现在成绩明细中。
示例输入:
```
2024012001, 2024012002
2024012015
```
## 清除本卷设置
弹窗左下角 **清除本卷设置** 会删除该试卷的全部独立配置(满分、合格率、免考),
之后完全跟随全局。
## 存储
| 环境 | 位置 |
| --- | --- |
| 启用 `pdo_sqlite` | `data/ny.db` 表 `ny_project_config`(主键 `project_id`) |
| 未启用 | `data/project_config.json` |
## 相关
- [系统设置](./settings.md)(全局默认值)
- [成绩明细与统计口径](./scores.md)