docs: refresh public docs for current codebase, add SECURITY.md
- README: real counts (44 commands, 61 integration asserts, demo=25 records + 4 semesters + 7 attachments), add resume/assessment module features, env vars incl. LSKY, doc navigation table, done-roadmap items checked - docs/api.md: /auth/logout, admin users/demo-rebuild/sync-lsky routes, attachment thumb/download channels, profile_upsert top-key merge semantics, full footprint_list query reference, 44-command table - sync-server/README: replace stale ADMIN_TOKEN/X-Admin-Token admin section (actual is_admin Bearer model), add go-live checklist, backup note for WAL - CHANGELOG: add Unreleased section (resume/assessment modules, security fixes, perf tuning) - CONTRIBUTING: 61 asserts, security-sensitive-change guidance, perf baseline table, security issue routing - docs/SECURITY.md: new - report channel, threat model table, hardening summary, known tradeoffs - LICENSE: ensure MIT file matches README badge
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
# 安全说明
|
||||
|
||||
## 漏洞报告
|
||||
|
||||
发现安全问题请**不要**直接开公开 Issue(可能包含可利用细节)。联系渠道:
|
||||
|
||||
- 仓库主页留下联系方式的维护者(站内私信 / Issues 留言板)
|
||||
- 紧急情况可直接在描述安全隐患的前提下开 Issue 标注 `security`,我们会尽快下线和修复
|
||||
|
||||
处理流程:收到报告 → 48 小时内确认 → 评估影响并给出缓解策略 → 修复发版 → 在 `CHANGELOG.md` 的 `Security` 段致谢公示。
|
||||
|
||||
## 安全模型(威胁模型摘要)
|
||||
|
||||
数据全走 HTTPS + Bearer Token,服务器为多用户托管形态,按用户强制隔离——信任边界假设
|
||||
「攻击者 = 其他注册用户 / 网络中间人 / 触碰 GET 接口的匿名流量」。
|
||||
|
||||
| 面 | 现状 |
|
||||
|---|---|
|
||||
| 认证算法 | scrypt(随机 16B 盐)+ `timingSafeEqual`,官方 crypto 原语,无自造算法 |
|
||||
| Token | 服务端 32 字节随机 hex 存储 + 365 天过期;登录后同设备复用 device_id;支持 `/auth/logout` 服务端吊销 |
|
||||
| 类似爆破的防护 | 登录:同 IP+用户名连续失败 5 次 → 冷却 10 分钟(进程内计数) |
|
||||
| 按用户数据隔离 | `commandsFor(user_id)` 每用户一份独立 `users/<uid>.db`;跨库读取天然命中"不存在",实测经 /api 与 /sync 均无法跨读 |
|
||||
| 管理 API | 统一双重校验:登录态 + is_admin;自删与"最后管理员"禁删 |
|
||||
| SQL 注入 | 全部参数化;`ORDER BY` 走白名单(`SORTS` 表);FTS MATCH 参数传入、字符串仅做双引号包裹 |
|
||||
| 路径注入/XSS | 上传文件名剥离 `\\/:*?"<>|` + UUID 前缀;附件文件名以 **SHA-256 命名**(无文件系统注入位);前端 `dangerouslySetInnerHTML` 有且仅两处(一处全转义、一处白名单净化 docx/xlsx 预览) |
|
||||
| 注册准入 | `REGISTER_ENABLED`、`INVITE_CODE`(或管理页动态码)、`MAX_USERS` 三道门控,服务端强制(客户端传的 `seed` 播种标志一律剥离,防止绕过邀请码/配额/自动升级管理员规则) |
|
||||
| 附件上传 | SHA-256 声明校验 + 100MB 上限;下载统一 `Cache-Control: private, immutable`(跨账号缓存隔离) |
|
||||
| 客户端依赖 | 服务器**零 npm 依赖**(仅 Node 内置模块),规避供应链攻击面 |
|
||||
| CORS | `Access-Control-Allow-Origin: *`。凭据仅 localStorage Bearer、不用 Cookie,不放大 CSRF 面;如有同源严格化诉求欢迎提案 |
|
||||
| 审计日志 | 图床清理、用户删除、demo 重建等敏感操作写标准日志(容器 stdout) |
|
||||
|
||||
## 已知设计取舍
|
||||
|
||||
- Token 存放浏览器 `localStorage`(XSS 可及):与多端 token 缓存目录一致;服务端可能追加 CSP 响应头、可加入 HttpOnly Cookie 会话机制(改动契约,暂未引入)
|
||||
- 限速是进程内存态,重启清零;RLS 想要更强的反爆破可以套容器内的 fail2ban / Cloudflare 规则
|
||||
- 备份文件内含用户档案明文,请按敏感数据管理
|
||||
|
||||
## 上线运维要点(契约要求)
|
||||
|
||||
- 首个真实账号注册后关闭 `REGISTER_ENABLED`,改掉 `INVITE_CODE` 示例默认值
|
||||
- `./data/` 目录备份(含 `-wal`/`-shm`)
|
||||
- 服务端公网面只暴露 443(Caddy TLS 到 sync-server)
|
||||
+48
-17
@@ -1,7 +1,7 @@
|
||||
# HTTP API 参考
|
||||
|
||||
Base URL:部署域名(如 `https://footpr.example.com`)。除 `/health` 与 `/auth/register|login` 外,
|
||||
所有接口都需要 `Authorization: Bearer <token>`。
|
||||
所有接口都鉴权。登录限速:同一 IP + 用户名连续失败 5 次会收到 429,冷却 10 分钟。
|
||||
|
||||
统一响应格式:
|
||||
|
||||
@@ -20,29 +20,32 @@ Base URL:部署域名(如 `https://footpr.example.com`)。除 `/health`
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| POST | `/auth/register` | 注册(`REGISTER_ENABLED`/`INVITE_CODE`/`MAX_USERS` 门控;首位用户自动成为管理员) |
|
||||
| POST | `/auth/register` | 注册(`REGISTER_ENABLED`/邀请码/`MAX_USERS` 门控;首个非 demo 账号自动成为管理员)。请求体里的 `seed` 标志一律被服务端剥离,仅供内部播种 |
|
||||
| POST | `/auth/login` | 登录,返回 Bearer Token(365 天) |
|
||||
| POST | `/auth/logout` | 吊销当前 Bearer Token(服务端删除 token 行) |
|
||||
| GET | `/auth/me` | 当前用户信息(`user_id` / `username` / `is_admin` / `device_id`) |
|
||||
|
||||
```bash
|
||||
curl -s https://footpr.example.com/auth/login \
|
||||
TOKEN=$(curl -s https://footpr.example.com/auth/login \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"username":"demo","password":"demo1234","device_name":"cli"}'
|
||||
# => {"ok":true,"data":{"token":"...","user_id":"...","device_id":"...","expires_at":"..."}}
|
||||
-d '{"username":"demo","password":"demo1234","device_name":"cli"}' | jq -r .data.token)
|
||||
```
|
||||
|
||||
## 管理(仅 is_admin 会话)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/admin/users` | 用户列表(设备数/记录数/附件用量) |
|
||||
| GET | `/admin/invites` | 邀请码列表(含 remark、创建者、使用者) |
|
||||
| GET | `/admin/users` | 用户列表(设备数/记录数/附件用量),元数据不含档案内容 |
|
||||
| POST | `/admin/users/delete` | 删除用户 `{"user_id":"..."}`,级联清理图床/档案库/附件/会话;不能删自己、不能删最后一位管理员 |
|
||||
| GET | `/admin/invites` | 邀请码列表(remark、创建者/使用者、valid、holder_gone) |
|
||||
| POST | `/admin/invites/generate` | 批量生成随机码 `{"count": 5}`(1-50) |
|
||||
| POST | `/admin/invites/create` | 新建自定义码 `{"code":"XIONG-2025","remark":"备注"}`(4-32 位字母数字-\_) |
|
||||
| POST | `/admin/invites/create` | 新建自定义码 `{"code":"XIONG-2025","remark":"备注"}`(4-32 位字母数字-_) |
|
||||
| POST | `/admin/invites/update` | 改备注 / 启停 `{"id":1,"remark":"...","valid":false}`(已使用的码不可再启用) |
|
||||
| POST | `/admin/invites/delete` | 删除 `{"id":1}` |
|
||||
| POST | `/admin/invites/revoke` | 作废(等价 update valid=false,兼容保留) |
|
||||
| POST | `/admin/demo-seed` | 给 demo 账号播种演示数据(幂等;档案库非空时返回 seeded:false) |
|
||||
| POST | `/admin/demo-rebuild` | 级联删除现有 demo 并重建、重播种演示数据 |
|
||||
| POST | `/admin/demo-seed` | 给 demo 播种演示数据(幂等;档案库非空时返回 seeded:false) |
|
||||
| POST | `/admin/attachments/sync-lsky` | 把当前用户全部未上图床的附件批量推送到 Lsky(需配置 `LSKY_API`/`LSKY_TOKEN`) |
|
||||
|
||||
## 同步
|
||||
|
||||
@@ -51,12 +54,15 @@ curl -s https://footpr.example.com/auth/login \
|
||||
| GET | `/sync/status` | 当前用户同步账本统计 |
|
||||
| POST | `/sync/push` | 推送 wire 变更(LWW 上/删除,立即生效) |
|
||||
| GET | `/sync/pull?cursor=...&limit=...` | 按 seq 游标增量拉取 |
|
||||
| POST | `/sync/attachment` | 上传附件 `{"sha256","size","mime","file_name","data_b64"}`(内容寻址去重) |
|
||||
| GET | `/sync/attachment/<sha256>` | 下载附件 |
|
||||
| POST | `/sync/attachment` | 上传附件 `{"sha256","size","mime","file_name","data_b64"}`(内容寻址去重,哈希校验) |
|
||||
| GET | `/sync/attachment/<sha256>` | 下载附件(`Cache-Control: private, immutable`) |
|
||||
|
||||
附件另有两个业务命令通道:`POST /api/attachment_read`(base64 全文,含缩略图通道 `attachment_thumb`)
|
||||
与 `POST /api/attachment_download`(直接落盘,`Content-Disposition` 带原始文件名)。
|
||||
|
||||
## 业务命令层 `POST /api/<command>`
|
||||
|
||||
与桌面端共用同一命令层(`app/server/db.mjs`),按用户隔离。37 个命令:
|
||||
与桌面端共用同一命令层(`app/server/db.mjs`),按用户隔离。44 个命令:
|
||||
|
||||
| 分组 | 命令 |
|
||||
|---|---|
|
||||
@@ -65,11 +71,39 @@ curl -s https://footpr.example.com/auth/login \
|
||||
| 字典/维度 | `taxonomy_list` · `taxonomy_upsert` · `taxonomy_delete` · `dimension_list` · `dimension_upsert` · `dimension_delete` |
|
||||
| 足迹 | `footprint_list` · `footprint_get` · `footprint_upsert` · `footprint_delete` · `timeline` |
|
||||
| 统计 | `stats_overview` · `metric_list` · `metric_upsert` · `metric_delete` |
|
||||
| 成绩 | `grades_overview` · `semester_upsert` · `semester_delete` · `course_upsert` · `course_delete` · `band_upsert` · `band_delete` |
|
||||
| 附件 | `attachment_add` · `attachment_list` · `attachment_delete` · `attachment_read` |
|
||||
| 成绩 | `grades_overview` · `semester_upsert` · `semester_delete` · `course_upsert` · `course_delete` · `band_upsert` · `band_delete` · `gpa_settings_get` · `gpa_settings_upsert` |
|
||||
| 附件 | `attachment_add` · `attachment_list` · `attachment_delete` · `attachment_read` · `attachment_thumb` · `attachment_set_remote` · `attachment_set_thumb` |
|
||||
| 档案 | `profile_get` · `profile_upsert` |
|
||||
| 保存视图 | `view_list` · `view_upsert` · `view_delete` |
|
||||
| 播种/同步内核 | `demo_seed` · `sync_apply_wire` · `sync_pull_wire` |
|
||||
|
||||
### `profile_upsert` 语义
|
||||
|
||||
顶层 key 合并:`info` / `resume` / `zongce` 是互相独立的子文档,只更所提交的 key,
|
||||
其余原样保留(避免三张页面互相覆盖):
|
||||
|
||||
```json
|
||||
{ "info": { "name": "...", "phone": "...", "eduStart": "2023-09" } }
|
||||
```
|
||||
|
||||
### `footprint_list` 的 `query`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `q` | 全文检索(≥3 字符走 FTS5 trigram 子串匹配,短词回退 LIKE) |
|
||||
| `year` | 起始年份过滤 |
|
||||
| `types` / `levels` / `roles` / `statuses` | 数组多选过滤 |
|
||||
| `dims` | 维度多选 |
|
||||
| `polarity` | `positive` / `negative` |
|
||||
| `has_attachment` | 仅看有附件记录 |
|
||||
| `tag` | 标签匹配 |
|
||||
| `sort` | `date_desc`(默认)/ `date_asc` / `updated_desc` / `weight_desc` |
|
||||
| `page` /`page_size` | 分页(`page_size` 上限 2000) |
|
||||
|
||||
### 分页约定
|
||||
|
||||
`page_size` 上限 2000;传非 `{query:{...}}` 形状的载荷会按默认 20 条处理(客户端请按此约定传参)。
|
||||
|
||||
示例(检索记录):
|
||||
|
||||
```bash
|
||||
@@ -77,6 +111,3 @@ curl -s https://footpr.example.com/api/footprint_list \
|
||||
-H 'content-type: application/json' -H "authorization: Bearer $TOKEN" \
|
||||
-d '{"query":{"page":1,"page_size":20,"year":2025}}'
|
||||
```
|
||||
|
||||
`footprint_list` 的 `query` 支持:`year`、`type_id`、`level`、`status`、`q`(全文检索)、
|
||||
`dimension_id`、`sort`(date_desc/date_asc/updated_desc/weight_desc)、分页 `page`/`page_size`。
|
||||
|
||||
Reference in New Issue
Block a user