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 行删除
+49
查看文件
@@ -341,9 +341,58 @@ PROFILE_EMAIL_MAX = 128
# 拆开写很容易出现「开了强制 HTTPS 却忘了 Secure」这类半截配置。
# ---------------- 落盘权限(对外部署的隐私基线)----------------
# 数据目录里躺着的东西比想象中敏感:
# * usage_records.prompt —— 用户与 AI 的完整对话正文
# * settings.value —— 凭证密文;instance.json —— 拆密文的主密钥
# * exports/*.csv —— 上面那份对话正文的**明文**副本
# * backups/*.zip —— 「全库 + 主密钥」的压缩包
# 默认 umask 022 会把它们留成 0644 / 0755,也就是**同机任何用户都能读**。
# 在对外提供服务的机器上,这不是理论风险(多租户主机、共享 CI runner、
# 被入侵的低权限进程都是现实场景)。
#
# 处理方式分两层:
# 1. `harden_process()` 把进程 umask 收到 0077 —— 此后本进程新建的**一切**
# 文件都天然是 owner-only(SQLite 的 -wal/-shm 也包括在内)。
# 刻意选择「一条设置管全部」而不是逐个 chmod:逐点 chmod 一定会漏,
# 而漏掉的那个文件里往往正好是最新的对话正文。
# 2. `harden_dir()` / `harden_file()` 用于收紧**已经存在**的目录与文件
# (老版本留下的 0755 目录、手工拷进来的备份等)。
# Windows 没有 umask 这一套(权限走 ACL),两个函数直接返回。
def harden_process():
"""把进程 umask 收紧到 0077。必须在任何写盘动作之前调用。"""
if os.name != "posix":
return
try:
os.umask(0o077)
except OSError:
pass
def harden_dir(path, mode=0o700):
"""把目录权限收紧到 owner-only。失败不致命(只读 / 网络文件系统)。"""
if os.name != "posix" or not path:
return
try:
os.chmod(path, mode)
except OSError:
pass
def harden_file(path, mode=0o600):
"""把文件权限收紧到 owner-only。渲染/导出/备份落盘后调用。"""
if os.name != "posix" or not path:
return
try:
os.chmod(path, mode)
except OSError:
pass
def ensure_dirs():
for d in (DATA_DIR, LOG_DIR, EXPORT_DIR, BACKUP_DIR):
os.makedirs(d, exist_ok=True)
harden_dir(d)
def _instance_read():