Skip to content

fix(security): OpenAI 端点 voice 参数存在性探针 + 17 条 path-injection 复核结论 - #83

Merged
ReSerendipity merged 1 commit into
mainfrom
fix/openai-voice-path-probe
Sep 20, 2026
Merged

ReSerendipity merged 1 commit into
mainfrom
fix/openai-voice-path-probe

Conversation

@ReSerendipity

Copy link
Copy Markdown
Owner

分诊表 §4 里 P1 的那 17 条 py/path-injection 逐条复核完了(training 9 / openai_api 4 / audio 4),结论分三类:

条数 结论
routes/audio.py 4 误报_safe_file_path 三层防御(白名单→强制拼接 root_dirresolve()+relative_to 含 symlink 判定),glob 命中后再复核一次归属
routes/training.py 9 已缓解pretrained_path/train_manifest/save_path 都在 os.makedirs 之前无条件_validate_pathrealpath + startswith(base + os.sep),写法不带同名兄弟目录漏洞);CodeQL 不建模这两个自定义净化器
openai_api.py 2(816/1008) 真问题,本 PR 修os.path.exists(os.path.join(PERSONA_DIR, f"{body.voice}.wav")) 把请求体原样拼进路径,命中继续合成、不命中才 400 —— 响应差异即存在性预言机(受 .wav 后缀约束,可读面有限但可探测)
openai_api.py 2(220/941) sink 是应用自生成的输出路径(_stream_file / final_path),非请求可控

改法

新增 _persona_wav_exists()realpath 目录归属判定 + isfile刻意不用 _validate_persona_name 的字符白名单——那会误伤早期登记、名字里带空格或全角字符的音色;遍历由 containment 就足够挡掉。顺带删两处多余的 import os as _os

证据(同夹具新旧对照)

相对越界 ../../../…/secret_target   旧 -> True    新 -> False
绝对路径名(join 会丢弃 dir)        旧 -> True    新 -> False
同名兄弟目录 personas_evil/trap      新 -> False
合法音色 alice                       新 -> True

tests/test_openai_voice_path_probe.py 5 passed;ruff check/format 过。

未包含

分诊表 §2.2/§4 的文档同步留到 #81/#82 合入后单独一小步,避免两条并行分支改同一个 docs/SECURITY_CODEQL_TRIAGE.md 打架。openai_api.py 不在完整性清单核心模块内,无需重签。

分诊 P1 逐条复核 17 条 py/path-injection 的结论(training 9 / openai_api 4 / audio 4):

- routes/audio.py 4 条:误报。_safe_file_path 是三层(字符白名单→强制拼接 root_dir→
  resolve()+relative_to 含 symlink 判定),glob 命中后再复核一次目录归属。
- routes/training.py 9 条:已缓解。pretrained_path / train_manifest / save_path 三处
  都在 makedirs 之前无条件过 _validate_path(realpath + startswith(base + os.sep),
  写法没有同名兄弟目录漏洞)。CodeQL 不建模这两个自定义净化器。
- openai_api.py 816/1008 两条:**真问题**。`os.path.exists(os.path.join(PERSONA_DIR,
  f"{body.voice}.wav"))` 把请求体原样拼进路径,命中就继续合成、不命中才 400 ——
  响应差异等于一个存在性预言机(需以 .wav 结尾,故可读面受限但仍可探测)。
  另两条 sink(_stream_file / final_path)用的是应用自己生成的输出路径,非请求可控。

改法:新增 _persona_wav_exists,只做 realpath 目录归属判定 + isfile。
刻意不用 _validate_persona_name 的字符白名单——那会误伤早期登记、名字含空格或全角
字符的音色;遍历由 containment 挡掉即可。顺带删掉两处多余的 `import os as _os`
(模块顶层早已 import os)。

证据(同一夹具下新旧对照实测):
  相对越界 ../../../tmp/secret_target  旧 -> True   新 -> False
  绝对路径名(os.path.join 会丢弃 dir)旧 -> True   新 -> False
  同名兄弟目录 personas_evil/trap       新 -> False
5 条回归测试全过(tests/test_openai_voice_path_probe.py);ruff check/format 过。

Signed-off-by: ReSerendipity <zengyangc@outlook.com>
ReSerendipity added a commit that referenced this pull request Sep 20, 2026
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
ReSerendipity merged commit 9c91040 into main Sep 20, 2026
30 checks passed
@ReSerendipity
ReSerendipity deleted the fix/openai-voice-path-probe branch September 23, 2026 07:15
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