feat(ci)+fix(security): 钉版下界棘轮门禁 + CSRF 静默降级改硬失败 - #81
ReSerendipity wants to merge 7 commits into
Conversation
方向查证后确认:4.57.0 是本项目自己声明的下界,且写了两处权威位置 (pyproject.toml:59 带理由「VoxCPM2 / IndexTTS2 的 tokenizer 与 modeling 需要 较新 transformers API」+ requirements.txt),check_engine_compat.py 也按它检测。 所以违规的是两个 lock 钉的 4.52.1 —— 随便携包分发的就是低于自家下界的 transformers。 脚本实测:25 个声明下界 / 93 个钉版 / 1 处违规(transformers)。 不猜该改哪边,只把冲突摊开;CI 侧用 --allow-debt transformers 棘轮化 (名单内只报不拦,名单外新增即红),避免为了存量债务给每个 PR 挂常红灯。 放在 Security Scan 的 pip-audit job 里 —— 依赖问题的车道,且非必需检查。 同时把 docs/release-governance.md §2 的两处失真改掉: - 「git push 触发 release-please.yml」→ main 实际不可直推(GH006 + enforce_admins), 只有 PR 合入那一刻才触发; - 补上发布前置断言:本脚本 0 违规,且白名单应逐次清空。 自测中发现并修掉本脚本自己的一个 bug:_parse_ver 把 +cu132 的数字当成第四段, 会让 2.13.0+cu132 压过 1.2.3.4;已加本地版本段与预发布尾标的剥离及三条回归测试。 14 个用例全过,退出码语义实测:strict=1 / 命中白名单=0 / 白名单不匹配=1。 Signed-off-by: ReSerendipity <zengyangc@outlook.com>
create_app() 此前在 data/.csrf_secret 写不出来时只 logger.warning,然后拿 **空密钥**继续 add_middleware(CSRFMiddleware) —— 一次磁盘满/权限故障就让 CSRF 防护自我关闭,且与「警告后照旧继续」的既定纪律冲突。 - 默认 raise RuntimeError,消息自带出路(让 data/ 可写;compose 已是可写挂载); - 只读部署需显式 TTS_ALLOW_EPHEMERAL_CSRF=1,此时用内存态强随机密钥 (比空密钥强:进程内仍有签名),并 warning 说明重启后 token 全失效; - .env.example 记录该开关。 - app_server.py 属核心模块,哈希入册,已同步重算并重签清单。 验证(真跑,非 mock):把 _PROJECT_ROOT 指到不存在的盘符制造真实 OSError, - 默认路径:create_app 抛 RuntimeError,断言含 "CSRF" 且含可操作关键词; - 开关路径:警告先落、流程越过 CSRF 门后才在别处撞盘符,断言逃逸的不是 CSRF 硬失败。 两个用例 + 既有 csrf/route-uniqueness/security_expanded/voice_clone_consent 共 45 个用例全过。 Signed-off-by: ReSerendipity <zengyangc@outlook.com>
dismiss 前先确认重扫:最近一次 CodeQL 分析 2026-09-19T18:05:48Z @ 552b0c6 (当前 main),results=97;逐条 GET 校验 state 与路径未变才 PATCH。 Signed-off-by: ReSerendipity <zengyangc@outlook.com> #54-#60(emoji 码位区间)false positive、#111 #112(tests/ 静态断言)used in tests、 #53(critical)mitigated。open 110 → 99,critical 1 → 0。 §4 里两行划掉:CSRF 静默降级已在同 PR 改硬失败。 沉淀两条 API 口径,避免下次重踩:dismissed_comment 上限 280 字符(长判据只能放本表, 注释里带 §号引用);dismissed_reason 是人读枚举 "false positive"/"used in tests"/ "mitigated",写 false_positive 会 422。另记一条旁证:#53 汇点从 :460 推移到 :468, 正是我加在函数入口的 8 行守卫把它挤下去的。
按 PyPI 元数据实测,transformers 4.57.0 要求 tokenizers>=0.22.0,<=0.23.0, 而我们钉的是 0.21.0 —— 所以清这笔债不是单包 bump,必须连带 tokenizers 一起动; 其余关键包不受阻(huggingface-hub 0.36.2 满足 >=0.34,<1.0;numpy/safetensors/ pydantic 均满足),requirements.txt:6 对 tokenizers 只声明 >=0.19.0, vendor 侧也没有钉死 0.21.0。 给出可复现的三步(先 --dry-run 解析、再 real 模式重建便携包过门禁、最后删白名单), 不代做:解析与真机构建都需要 ≥60GB 的 self-hosted runner,仓库当前 runner 数为 0。 Signed-off-by: ReSerendipity <zengyangc@outlook.com>
不是 pytest 绿当测过:真起 start_portable.py,加载 voxcpm2(冷 35s), 合成 15 字中文得 2.80s / 48kHz / 16bit 音频,RMS 5395、peak 30003 证明确实出声, nvidia-smi 独立佐证显存 11204/12227 MiB、利用率 54%,RTF 1.46, unload 与 /api/system/shutdown 均 200 干净退出。 如实写明边界:证据由两次运行拼成(第一次 RMS 校验代码有 bug、 第二次模型已在显存里所以「未加载 503」那格由第一轮供证), 人耳听感仍归人工;release-gate real 模式未跑,缺 WinPython 与 torch 轮子两个输入(补齐属大批量下载,不擅自执行),磁盘 141GB 不是瓶颈。 Signed-off-by: ReSerendipity <zengyangc@outlook.com>
audio 4 条误报(_safe_file_path 三层 + glob 复核)、training 9 条已缓解 (三处 makedirs 前无条件 _validate_path,且 startswith(base + os.sep) 写法无 同名兄弟目录漏洞)、openai 输出路径 2 条误报;openai voice 2 条是真问题, 留给 #83 修 + 重扫,不在这里提前收口。累计 dismiss 25 条,open 110 → 85。 Signed-off-by: ReSerendipity <zengyangc@outlook.com>
本 PR 的 §1.1 债务卡有一条建议已被后续证据推翻,合并前请改掉我在 可证明的现状(详见 issue #97): 修法应改成两选一:
另外本 PR 的 (本地这个 checkout 正在被并发使用,我不再切分支改文件;这条更正请在合并时一并处理, |
|
逐文件核对结论(2026-09-21,对着当前 ① 仍缺、且值得单独重开一条 —— CSRF 的静默降级 except OSError as csrf_err:
logger.warning("[create_app] CSRF 密钥初始化失败,回退到无签名模式: %s", csrf_err)也就是密钥文件读不出来时警告一声就继续跑, ② 已被 main 上的等价判据取代 —— 地板棘轮 ③ 按约定归仓库所有者 —— 完整性清单与签名 ④ 文档两件 —— 文件已在 main,内容未逐行比对 建议处置:不整分支合。把 ① 单独重开成一个小 PR( |
现状(`app_server.py:808-821`,本仓自 CSRF 落地起一直如此):读 `data/.csrf_secret` 抛 OSError
时只 `logger.warning("回退到无签名模式")`,服务照常起、照常接请求 —— 只是 CSRF token 从此没有
HMAC 绑定。目录只读 / 磁盘满 / 权限被改任一情况都会静默削弱一道安全机制,与"前置条件不满足要
硬失败并给可操作建议"的口径相反(同一形状的静默降级在 #132、OpenAI 口 500 上都复现过)。
这也是 #81 唯一还留着的实质内容(它的地板棘轮部分已被 check_pin_crossconflicts 进 CI +
tests/test_dependency_consistency.py 的 D1/D2 + #106 取代)。
改动:
- `app_server`:密钥解析抽成 `_load_or_create_csrf_secret(path)`,任何失败一律 `RuntimeError`,
消息点名路径并给排查方向(目录可写 / 磁盘 / 容器只读根 fs 时 data 要走卷)。
- `CSRFMiddleware.__init__`:空 `secret_key` 直接 `ValueError`,装配期就拦,防止别处 new 一个
空密钥把这条规矩绕过去。
- 清单与签名已重生成/重签(`generate_integrity_manifest.py` → `sign_integrity_manifest.py`),
`--verify` 通过、`check_integrity_manifest_sync.py` exit 0。
一个必须记下来的坑(第一版就踩了):**清单要在行尾归一化之后生成**。
`git add` 时按 `.gitattributes`(`*.py text eol=lf`)把 CRLF 归一化成 LF,而我第一次是在还带
CRLF 的工作树上生成清单的 → 清单里存的是"CRLF 字节"的哈希,签名对这份清单本身**完全有效**
(`--verify` PASS!),但运行时自检按 LF 检出结果比哈希 → `run_startup_selfcheck(enforce=True)`
判定 `app_server.py`、`middleware/csrf.py` 校验失败。也就是说"验签通过"不等于"清单是对的",
两者要分别验。修法是归一化后重新生成再签。
顺带更正一处测试里的错前提:`test_csrf_integration.py` 两个用例拿裸字符串 "x" 同时当 cookie 和
header,那只对"空密钥=不签名"的旧行为成立;配了密钥之后中间件确实会做 HMAC 校验,于是改成先
GET 取一枚中间件自己签发的 token —— 这同时证实签名校验这条链路是生效的,不是摆设。
验证:新增 `tests/test_csrf_secret_hardfail.py` 5 条(生成并落盘 / 空白文件重生成 / 不可写位置
硬失败且文案含出路 / 空密钥装配期拒绝 / 有密钥仍可装配=防空转);`test_csrf_integration.py` 13 条;
`test_secret_key_and_manifest_signature.py` 12 条全过;**全量 2094 passed / 0 failed**;
ruff / format 干净,mypy 维持 103 = 基线。
Signed-off-by: ReSerendipity <zengyangc@outlook.com>
|
按当初的分诊关掉这条 —— 三块内容现在各有去处,逐块交代清楚,避免以后有人来这条里找已落地的东西。 1.
|
承接 #79 报告里「待你决策」的两条,按建议落地。
1. 钉版 vs 声明下界(
scripts/check_pin_floors.py)方向查证后与 #79 报告里的猜测相反:
>=4.57.0不是笔误,它在两处权威位置(
pyproject.toml:59带理由「VoxCPM2 / IndexTTS2 的 tokenizer 与 modeling 需要较新transformers API」+
requirements.txt),check_engine_compat.py也按它检测。违规的是两个 lock 钉的
4.52.1—— 即随便携包分发的 transformers 低于自家下界。脚本实测:25 个下界 / 93 个钉版 / 1 处违规(transformers,两个文件各钉一次)。
不擅自改任何版本钉:把 4.52.1 升到 4.57 需要重做便携包依赖解析并在真机验证
(当初就是为了
ResolutionImpossible才对齐到 .venv 实测值的)。所以本 PR 只负责让它可见且不再扩大:CI 里用
--allow-debt transformers棘轮化——名单内只报不拦,名单外新增即红。放在
Security Scan的 pip-audit job(依赖车道,非必需检查),不给每个 PR 挂常红灯。
docs/release-governance.md§2 写成发布前置断言。顺带修掉脚本自己的一个 bug(自测抓到):
_parse_ver把+cu132的数字当第四段,会让
2.13.0+cu132压过1.2.3.4。14 个用例,退出码实测 strict=1 / 命中白名单=0 / 不匹配=1。2. CSRF 静默降级改硬失败
create_app()此前在data/.csrf_secret写不出来时只 warning,然后用空密钥继续挂
CSRFMiddleware:一次磁盘满/权限故障就让 CSRF 防护自我关闭。raise RuntimeError,消息自带出路;已核实docker-compose.yml:39把./data挂成可写,所以 Docker 部署不受影响;TTS_ALLOW_EPHEMERAL_CSRF=1,此时用内存态强随机密钥(比空密钥强:进程内仍有签名)并 warning 说明重启后 token 全失效;
app_server.py属核心模块 → 已同步重算并重签integrity_manifest.json。验证(真跑 OS 级失败,不是 mock)
把
_PROJECT_ROOT指向不存在的盘符:RuntimeError: CSRF 密钥不可用(持久化失败: [WinError 3]…)拒绝以无签名模式启动…按 TTS_ALLOW_EPHEMERAL_CSRF=1 使用内存态 CSRF 密钥先落,流程越过 CSRF 门后才在别处撞盘符(断言据此设计,不假装后续成立)。
test_csrf_integration/test_route_uniqueness/test_security_expanded/test_voice_clone_consent一起跑:45 passed。未覆盖
real模式门禁(self-hosted + 27GB 真推理):仓库当前actions/runners数为 0,跑不了,桌面产物仍未真机验收。