Skip to content

feat(ci)+fix(security): 钉版下界棘轮门禁 + CSRF 静默降级改硬失败 - #81

Closed
ReSerendipity wants to merge 7 commits into
mainfrom
ci/pin-floors-and-csrf-hardfail
Closed

ReSerendipity wants to merge 7 commits into
mainfrom
ci/pin-floors-and-csrf-hardfail

Conversation

@ReSerendipity

Copy link
Copy Markdown
Owner

承接 #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
  • 本地 pre-commit 20 项钩子两次提交全过。

未覆盖

  • 存量债务本身(把便携钉版升到 >=4.57.0 并重做真机解析验证)——需要发布级动作。
  • real 模式门禁(self-hosted + 27GB 真推理):仓库当前 actions/runners 数为 0
    跑不了,桌面产物仍未真机验收。

方向查证后确认: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>
@ReSerendipity

Copy link
Copy Markdown
Owner Author

本 PR 的 §1.1 债务卡有一条建议已被后续证据推翻,合并前请改掉

我在 docs/release-governance.md §1.1 写的修法第一步是
「把钉版升到下界以上并重新做便携包解析」(潜台词:升到 >=4.57.0 即可)。
这条不成立:main 的锁里 tokenizers 已经被推到 0.23.2,而 4.x 全线
(实测 4.56.2 / 4.57.0 / 4.57.6)的天花板都是 tokenizers<=0.23.0 —— 升到 4.57.x
救不了它,只会把冲突从「低于下界」换成「高于上界」。

可证明的现状(详见 issue #97):transformers==4.52.1 要求 tokenizers<0.22
sympy==1.14.0 要求 mpmath<1.4hydra-core/omegaconf 要求 antlr4==4.9.*
—— 四条全是按 PyPI requires_dist 直接证伪的,锁集当前不可解析。

修法应改成两选一:

另外本 PR 的 check_pin_floors.py 只查下界,这四条它一条都抓不到 ——
#98 那个 check_pin_crossconflicts.py 才是补这个盲区的(实测在当前 main 上 rc=1
并逐条列出这 4 类 × 2 份锁)。两者是互补关系,建议在 §1.1 里互相指一下。

(本地这个 checkout 正在被并发使用,我不再切分支改文件;这条更正请在合并时一并处理,
或等并发工作结束后我来改。)

@ReSerendipity

Copy link
Copy Markdown
Owner Author

逐文件核对结论(2026-09-21,对着当前 main = 55229f8 核的,不凭印象)。本 PR 11 个文件分四类:

① 仍缺、且值得单独重开一条 —— CSRF 的静默降级
mainapp/integrated_app/app_server.py:808-821 现在还是这样:

except OSError as csrf_err:
    logger.warning("[create_app] CSRF 密钥初始化失败,回退到无签名模式: %s", csrf_err)

也就是密钥文件读不出来时警告一声就继续跑CSRFMiddleware 拿到空 secret。这与仓库口径
(资源/前置条件不满足要硬失败,不许"警告后照旧继续")相反。本 PR 的
tests/test_csrf_secret_hardfail.py(+48)与 .env.example(+8)就是冲它来的 —— 这部分内容有效。
(更正一句:我在别处说过"它的 CSRF 部分已以别的方式进 main",对这条回退路径是错的,它还在。)

② 已被 main 上的等价判据取代 —— 地板棘轮
scripts/check_pin_floors.py(+185)+ tests/test_check_pin_floors.py(+99)+ security.yml(+8)
做的事与现有三件重叠:check_pin_crossconflicts.py 已在 #101 接进 CI lint job;
tests/test_dependency_consistency.py 的 D1/D2(#103)核对"声明 ↔ 锁"并钉住
transformers==4.52.1 + tokenizers==0.21.0#106 又把 D1 扩到 launcher/requirements-small.txt
而且本 PR 的地板前提写在 >=4.57.0 上 —— 那个下界已被 A/B 实测推翻(4.57.6 下
indextts.infer_v2_5 / infer_v2 直接 ImportError,见 docs/SECURITY_DEPENDABOT_TRIAGE.md §2)。
再留一套并行判据只会两边漂移。

③ 按约定归仓库所有者 —— 完整性清单与签名
app/integrated_app/security/integrity_manifest.jsonintegrity_manifest.json.sig.ed25519
清单 + 私钥签名两件都在禁区里(权重/签名类改动需人工确认并 SHA-256 复验),我不动、也不代签。
这一条也是"别整分支合"的硬理由:合并会把签名一起带进去。

④ 文档两件 —— 文件已在 main,内容未逐行比对
docs/SECURITY_CODEQL_TRIAGE.mddocs/release-governance.md 在 main 上存在(本 PR 是对它们的
+62/+35 增补)。我只核对了存在性,没逐行比对增补是否已被别处覆盖 —— 要留哪个版本归所有者定。

建议处置:不整分支合。把 ① 单独重开成一个小 PR(app_server.py 的 OSError 分支改 raise +
带上 tests/test_csrf_secret_hardfail.py.env.example),② 关闭,③④ 交所有者判。
我没有替你关这个 PR,也没动那两处签名文件 —— 本 PR 里"关掉即失去 ①"这件事需要你确认过一遍。

ReSerendipity added a commit that referenced this pull request Sep 21, 2026
现状(`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>
@ReSerendipity

Copy link
Copy Markdown
Owner Author

按当初的分诊关掉这条 —— 三块内容现在各有去处,逐块交代清楚,避免以后有人来这条里找已落地的东西。

1. scripts/check_pin_floors.py(钉版下界棘轮):前提反了,不收

本 PR 的立论是">=4.57.0 是权威声明、违规的是 lock 里的 4.52.1"。实测相反:

  • indextts 2.0.0 元数据里钉的是 transformers==4.52.1tokenizers==0.21.0 ——
    4.52.1 不是"低于下界",它就是引擎能跑的那个版本;抬到 4.57 会让便携包/镜像里的
    IndexTTS 直接起不来(这正是当初对齐到 .venv 实测值的原因)。
  • 也就是说权威声明本身写错了方向。修法是把声明改回实测值并按公告逐条登记已接受风险,
    已由 fix(deps): 依赖下界回到实测能跑的 transformers 4.52.x,安全门禁改为逐条带理由的豁免 #103 落地(transformers>=4.52.1,<4.53 + tokenizers>=0.21.0,<0.22
    .trivyignore.yaml 3 条 + security.yml 里 16 个 PYSEC 豁免)。

所以这个 checker 现在的判据会把唯一正确的锁集判成违规,收进来就是常红灯。
"钉版 vs 声明"这条线仍有人守:tests/test_dependency_consistency.py 的 D1 已扩到
requirements-lock.txt + launcher/requirements-small.txt 两份清单(21 个包对齐声明),
另有 scripts/check_pin_crossconflicts.py 管互斥钉版。代价如实记一下
本 PR 那份 checker 自带的 14 个用例和 _parse_ver+cu132 本地版本的修正一起丢了;
如果以后还要做"下界棘轮",得先重定义为"引擎实际钉住的版本 ⊆ 声明区间",不能沿用本 PR 的判据。

2. CSRF 静默降级改硬失败:已落地,但不是这条 PR 的形状

#109 落地(合并状态以该 PR 为准)。两处与本 PR 的形状不同,读代码的人需要知道:

  • 没有 TTS_ALLOW_EPHEMERAL_CSRF 这个开关。当前口径是"任何取不到密钥的情况一律
    RuntimeError 拒绝启动",CSRFMiddleware.__init__ 另加一道空 secret → ValueError
    防别的装配点绕过。.env.example 因此也没引入该变量。
    理由:那条 fallback 本身就是"警告后照旧继续",用一个显式开关把它制度化的价值,
    低于"少一个能被误开的静默降级入口"。
  • 追加了真加固:密钥用 os.open(..., 0o600) 创建、老文件读后/写前 chmod 收紧并复核,
    3 条 POSIX 门控测试守着(含反空验证)。这顺带处理了 CodeQL 既有告警 chore: add cleanup report and non-destructive removal scripts (bot/cleanup-ignored-files-2026-08-06) #1
    py/clear-text-storage-sensitive-data)—— 加固 + sink 行带理由抑制,不整条静音。

3. docs/SECURITY_CODEQL_TRIAGE.md已救出来,见 #112

这份逐条读过代码的分诊表是唯一成体系的 CodeQL 台账,不跟着本 PR 一起丢。
#112 保留其 §0–§5 原文(2026-09-20 的定性快照),另按 09-21 实测刷新:
open 110 → 76、critical 1 → 0,34 条差额逐条对上账;
并复核了 §2.1 声称的 critical 入口守卫确在 main(persona_manager.py:436-440
_PERSONA_NAME_RE.fullmatchtests/test_persona_embedding_load.py)。

—— 关闭。分支 ci/pin-floors-and-csrf-hardfail 先留着,若以后要重做下界棘轮,
check_pin_floors.py 与它那 14 个用例在这条分支上可直接取用(删分支需要明确指示,我不代删)。

auto-merge was automatically disabled September 21, 2026 15:34

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant