feat(安全): 对外暴露面加固 + 界面去 AI 化(v1.5.0)

界面(去 AI 味):
- 大屏页清除 114 处生成器残留属性 data-page-node-id
- 视觉系统改回工程控制台风格:去 radial/linear-gradient、去辉光、
  去标题前彩色装饰条,改为中性灰阶 + 单一蓝色强调色;KPI 色条改状态点
- 精简各页说教式长提示;修掉 profile.html 泄漏到页面上的 Markdown 星号
- 删除登录页过时的「默认账号 admin / admin123」提示(1.4.0 起已无默认口令)

安全与隐私(按「将会被公网访问」收口):
- 内部异常只回 8 位事件号,完整堆栈进服务端日志(web/api.py::_internal)
- 导出文件名收敛:防响应头注入与路径穿越;manage.py passwd 补用户名校验
- 登录对不存在的账号也走一次哑哈希,抹平用户名枚举的时序差异
- /api/* 读接口限速 240 次 / 60 秒 / 账号(挡住循环调 /api/bundle)
- 进程 umask 0077 + 目录 0700 / 文件 0600:对话正文与主密钥的落盘权限
- 表名与库文件路径只对管理员下发;大屏页所有数据插值转义
- --debug 只允许绑定回环地址;新增 Permissions-Policy 与 413 处理器

文档:
- DEPLOYMENT 新增第十三节「安全与隐私基线」;迁移表补 1.4.0 → 1.5.0 行
- SECURITY 更新支持范围、新增「信息泄漏收敛」小节与上线检查项
- .codebuddy/ 加入 .gitignore(助手工作记忆不进仓库)

版本:1.4.0 → 1.5.0(无库结构变更,user_version 仍为 4)
验证:python tools/smoke.py → ok=264 fail=0;python tools/check_docs.py → 0 处问题
这个提交包含在:
2026-09-18 11:13:17 +08:00
父节点 3751dffef9
当前提交 f36149efc3
共修改 29 个文件,包含 1105 行新增和 558 行删除
+110
查看文件
@@ -8,6 +8,116 @@
---
## [1.5.0] — 2026-09-18
**主题:按「将会被公网访问」重新审一遍界面与暴露面**
两件事:① 清掉界面里的 AI 生成残留与「AI 味」,把视觉从「霓虹渐变」改回工程控制台;
② 按「能被公网访问」的前提逐条收口信息泄漏、注入与资源滥用。
**没有库结构变更**(`user_version` 仍是 4),升级不需要手工介入。
### 安全与隐私加固(对外提供服务)
**主题:把「能被公网访问」当成前提,逐条收口信息泄漏与资源滥用**
这一轮没有新增功能,全部是收口。原则:**能把信息少给一点就少给一点,
能把权限多收一层就多收一层**。
**信息泄漏(这类问题的共同点是:平时看不出问题,踩点阶段最好用)**
- **内部异常不再直出**。`/api/collect`、`/api/maintenance/*`、`/api/backups*` 的兜底
`except Exception` 原本把 `str(e)` 原样回给客户端 —— 那里面会带绝对路径
(`/app/workbuddy_portal/...`)、SQL 语句、`sqlite3` 的报错。现在统一走
`web/api.py::_internal`:完整堆栈写进服务端日志并打一个 **8 位事件号**,
客户端只拿到事件号。既不影响「出事了去查日志」,也不送材料。
(`BadParam` / `Busy` / `NotReady` / `ApiError` / `BackupError` 这些**面向用户写的**
业务异常不受影响,它们的文案本来就是给用户看的。)
- **普通账号拿不到内部实现清单**。`/api/manifest`、`/api/bundle` 里的 `sources`
(表名 `usage_records`、`daily 聚合视图` …)与 `archive`(`data/usage.sqlite`)
只对管理员下发;非管理员的大屏页「数据源」卡片自动收起,避免留一个只有表头的空壳。
- **响应头注入**。导出文件名里含用户名,而用户名**并不总是**注册正则的产物
(`manage.py passwd` 建号时不校验、老库升级上来的名字也可能带引号或 CR/LF)。
一旦名字里有引号或换行,直接拼 `Content-Disposition` 就是响应拆分。
新增 `security.safe_filename()` 与 `security.content_disposition()`:
收敛成 ASCII 安全名 + 按 RFC 5987 附上原名,`/records/export` 与
`/profile/export` 都改走它。
- **路径穿越**。`collect.export_csv()` 的账号名 tag 直接拼进文件路径 ——
一个叫 `..\..\x` 的账号能把 CSV 写到数据目录之外。现在只保留 `[A-Za-z0-9._-]`,
且结果固定落在 `EXPORT_DIR` 之下。同时 `manage.py passwd` **建号时补上用户名校验**
(与注册页同一套),从根上不再产生这类名字。
- **用户名枚举的时序侧信道**。`login_ok` 在账号不存在时不执行任何哈希计算,
比「口令错误」快一到两个数量级,一个秒表就能枚举出哪些用户名真实存在。
现在对不存在的账号也走一次**同代价的哑哈希**,两条路径耗时对齐。
**隐私:落盘权限(这条是对外部署最容易被忽略的)**
- 数据目录里躺着的是:`usage_records.prompt`(**用户与 AI 的完整对话正文**)、
凭证密文、`instance.json`(主密钥)、`exports/*.csv`(对话正文的**明文**副本)、
`backups/*.zip`(全库 + 主密钥)。默认 `umask 022` 会把它们留成 `0644 / 0755`,
也就是**同机任何用户都能读**。
- 新增 `config.harden_process()`:在进程启动时把 `umask` 收到 `0077`,
此后新建的一切文件(含 SQLite 的 `-wal` / `-shm`)天然是 owner-only。
刻意选「一条设置管全部」而不是逐个 `chmod` —— 逐点 `chmod` 一定会漏,
而漏掉的那个文件里往往正好是最新的对话正文。
- 配套 `harden_dir()` / `harden_file()` 收紧**已经存在**的目录(老版本留下的 0755)
以及导出 CSV、备份 zip 落盘后的权限。Windows 无此模型,函数直接跳过。
**资源滥用**
- **读接口限速**:登录有 IP 锁定、注册有配额、采集有最小间隔,但读接口原本**没有任何刹车** ——
一个注册账号把 F12 里的 `/api/bundle` 请求放进 `for` 循环就能持续吃 CPU 与出口带宽
(那个接口要算全量逐日聚合,还会下发最多 2 万条明细)。现在 `/api/*` 按
「账号(未登录时按来源 IP)」限速 240 次 / 60 秒,超出返回 `429`。
阈值给得宽松:大屏切一次筛选只发 1~2 个请求,正常用户碰不到。
**其它**
- **`--debug` 不再能绑到对外地址**。Werkzeug 的交互式调试器等于任意代码执行,
而 `--host` 默认就是 `0.0.0.0`;`python manage.py serve --debug` 会直接把 RCE 挂上公网。
现在绑非回环地址直接拒绝启动,并提示本机调试的正确写法。
- 新增 `Permissions-Policy` 响应头(显式关掉地理位置 / 麦克风 / 摄像头 / 支付 / USB)。
- 新增 `413` 处理器:请求体超限时接口返回 JSON 而不是 Flask 默认 HTML 页
(前端 `r.json()` 在 HTML 上会炸,表现成「Unexpected token <」)。
- 文档:新增 [部署指南第十三节「安全与隐私基线」](DEPLOYMENT.md#十三安全与隐私基线) ——
代码层已做到的、公网暴露前必须自己做的、存了哪些个人数据、保留期怎么定、上线自检命令。
验证:`python tools/smoke.py` **ok=264 / fail=0**(第 9 节专门覆盖上述各项:
`safe_filename` 边界、真实导出响应头不含引号/换行、每个 `except Exception` 都走 `_internal`、
读接口限速、安全响应头、大屏页已转义、普通账号拿不到数据源清单)。
### 界面去 AI 化
**主题:清掉界面里的 AI 生成残留与「AI 味」**
- **大屏页清理生成器残留**:`static/dashboard/index.html` 里 **114 处**
`data-page-node-id="…"`(页面生成工具给每个节点打的标记)全部删除,
文件从 64 KB 降到 58 KB。这类属性没有任何运行时作用,只是生成痕迹。
- **视觉系统重做**(`static/css/app.css` + 大屏内联样式):由「深色底 + 霓虹渐变 + 辉光」
改为中性的工程控制台风格 —— 灰阶打底、单一蓝色强调色。具体删掉了:
`radial-gradient` 页面背景、按钮/激活态/分页/徽标的 `linear-gradient`、
`box-shadow` 发光(品牌点、主按钮、日历格)、渐变文字(错误页大号状态码)、
以及标题前的彩色装饰条(`.pagehead h1::before`、`.card h2::before`、`h1::before`)。
- **KPI 改用灰度分层**:原来每张卡一条彩虹色左色条,现在 `--c` 只渲染成一个 8px
状态点(`采集健康`/`自动备份` 这类状态卡仍能一眼区分),其余靠灰度与字重。
- **图表配色同步降饱和**:ECharts `PALETTE`、日历 5 级色阶、热力图 `visualMap`
与后台共用同一套语义色;数据系列改用蓝色阶 + 1 个琥珀色对比项。
- **文案精简**:删掉说教式长段落(`配置管理` 页整张「为什么采集参数是只读的」说明卡)、
把只给结论即可的提示压成一句话;`任务管理`/`配置管理`/`备份管理`/`用户管理`
的长提示同步收敛。
- **修掉两个真实缺陷**(都在这次清理中暴露):
- `templates/profile.html` 的 Markdown 星号漏进 HTML —— `**任何时候都不回传明文**`
会在页面上原样渲染成星号;
- 登录页写着「首次部署默认账号 `admin` / `admin123`」,而 1.4.0 起已改用随机口令,
该提示会把人引向一个永远登不上的口令,已删除。
- **文档同步**:`docs/USER-GUIDE.md` 中引用上述被删文案的两处说明一并更新。
界面截图 `docs/images/*.png` 仍是旧观感,需要用 `python tools/shots.py` 重出。
验证:`python tools/smoke.py` 全绿(ok=233 / fail=0),其中
「页面 class 与 app.css 选择器对账」确认新版样式表没有漏掉任何模板在用的类名;
`python tools/check_docs.py` 0 处问题。
---
## [1.4.0] — 2026-09-16
**主题:备份管理 · 对外提供服务的安全加固 · 资源与频率限制**