本次代码审查(全 12 个包,约 2400 行)的交付物:

- 9 项漏洞(高 1 / 中 2 / 低 4 / 信息 2)
  - 已核查无风险项 8 条
  - 工程债务记录(零测试、死代码、云盘占用)
  - 安全加固建议(Docker 密钥传递、TLS、依赖固定)

供后续按优先级处理备查。
"
This commit is contained in:
sun
2026-08-25 23:10:34 +08:00
parent 72d0d6a3a9
commit ed3d7c6b61
+197
View File
@@ -0,0 +1,197 @@
# 漏洞报告 — Volcano-Engine-TTS-UI
## 元信息
| 项目 | 内容 |
|---|---|
| 目标 | ByteDance TTS v3 → OpenAI 兼容接口适配器(Go) |
| 审查范围 | 全部 12 个包、约 2400 行源码(不含 health.html 前端静态页) |
| 审查方式 | 人工代码审查 + `go build` / `go vet`(均通过) |
| 分支/提交 | develop @ 78c7200 |
| 报告日期 | 2026 年 8 月 25 日 |
| 严重度分级 | 🔴 高(必须修复)/ 🟠 中(建议修复)/ 🟡 低(视部署环境)/ ⚪ 信息(记录备查) |
---
## 漏洞清单(按严重度)
| 编号 | 严重度 | 标题 | 位置 | 一句话影响 |
|---|---|---|---|---|
| VUL-004 | 🔴 高 | `.env` 未被 `.gitignore` 忽略,凭据可能入库/入镜像 | `.gitignore` | API Key 随 git 提交或 Docker 镜像层泄露 |
| VUL-001 | 🟠 中 | aac/flac 响应 Content-Type 与数据不一致 | controller/tts.go、adapter/volcano/synthesis.go | 客户端按 AAC 解码 MP3 数据,播放失败 |
| VUL-003 | 🟠 中 | `X-Forwarded-For` 信任链可伪造 IP 绕过限流 | middleware/ratelimit.go | 反代追加模式下限流失效 |
| VUL-002 | 🟡 低 | transport 层错误不进入 `UpstreamErrors` 指标 | metrics/metrics.go、adapter/volcano/synthesis.go | 网络故障在监控上不可见 |
| VUL-005 | 🟡 低 | 日志注入:客户端可控内容原样写入日志 | middleware/logger.go、controller/tts.go | 可伪造日志行 |
| VUL-006 | 🟡 低 | `/metrics`、`/health`、`/dashboard` 无鉴权 | router/router.go | 公网暴露时泄漏运行细节(设计权衡) |
| VUL-007 | 🟡 低 | `OPENAI_TTS_API_KEY` 未设置时鉴权完全关闭 | middleware/auth.go | 公网直连即无访问控制(设计权衡) |
| VUL-008 | ⚪ 信息 | speed 超范围静默截断 | adapter/volcano/request.go | 0.25~0.5x、2.0~4.0x 实际被 clamp,无提示 |
| VUL-009 | ⚪ 信息 | WAV 输出采样率依赖配置而非上游实际值 | adapter/volcano/audio.go | 配置错误导致音频变速 |
---
## VUL-004 🔴 高 — `.env` 未被忽略,凭据可能入库/入镜像
**位置**: `.gitignore`(全文件仅忽略构建产物与编辑器文件)
**描述**: README 与 `.env.example` 均指导用户执行 `cp .env.example .env` 后填入火山 API Key。但 `.gitignore` **没有包含 `.env`**。任何按此流程操作并执行 `git add .` / `git commit` 的用户,都会把含 `BYTEDANCE_TTS_API_KEY`、`OPENAI_TTS_API_KEY` 的文件提交进仓库历史(即使之后删除,历史中仍可找回)。Dockerfile 第 8 行 `COPY . .` 同样会把 `.env` 拷入镜像层。
**影响**: 火山账号 API Key 泄露 → 冒用额度、产生费用、音色资源被盗用。密钥一旦进入 git 历史或镜像层即视为已泄露,只能吊销重建。
**修复建议**:
```gitignore
# Secrets
.env
.env.*
!.env.example
```
**验证**: 当前工作区无 `.env` 文件,仓库历史也未发现已提交的 `.env`(已核查 `git log` 提交列表无该文件),属"配置隐患"而非"已泄露"。
---
## VUL-001 🟠 中 — aac/flac 响应 Content-Type 与真实数据不一致
**位置**: controller/tts.go:196-212(`contentTypeFor`)、adapter/volcano/synthesis.go:59-63、118-119
**描述**: 客户端请求 `response_format: "aac"`(或 `flac`)时,调用链为:
```
resolveClientFormat("aac") → "aac"(白名单放行)
synthesis: opts.Format = "mp3"(上游降级,正确)
synthesis: finalFormat = clientFormat = "aac"(错误,保留客户端格式)
controller: Content-Type = contentTypeFor("aac") = "audio/aac"(错误)
```
实际响应字节是 **MP3**,但 `Content-Type` 是 `audio/aac`。
**影响**: 客户端(浏览器 `<audio>`、播放器 SDK)按 AAC 解码器处理 MP3 流,轻则播放失败/杂音,重则解码崩溃。README 声称"降级到 mp3",但响应头未同步降级。
**修复建议**(二选一):
1. `synthesis.go` 在降级后把 `finalFormat` 置为实际上游格式(`mp3`);
2. `contentTypeFor` 对 `aac`/`flac` 直接返回 `audio/mpeg`。
推荐方案 1(响应头应反映真实数据)。
---
## VUL-003 🟠 中 — X-Forwarded-For 信任链可伪造 IP 绕过限流
**位置**: middleware/ratelimit.go:129-150(`GetClientIP`)
**描述**: `GetClientIP` 在直连 IP 为私有地址(即判定为反代)时,信任 `X-Forwarded-For` 的**第一个**值,其次信任 `X-Real-IP`。若反代(nginx 等)使用追加模式(`$proxy_add_x_forwarded_for`),攻击者发送 `X-Forwarded-For: 1.2.3.4`,反代追加真实 IP 后请求头为 `1.2.3.4, 真实IP`,代码取 `1.2.3.4`。
**影响**:
- 攻击者每次请求携带不同伪造 IP,即可绕过 100 次/分钟的 IP 限流(限流 key 由该函数返回值决定);
- 可伪装成受害 IP 请求,把受害 IP 的限流配额耗尽(间接 DoS)。
**前提**: 服务必须部署在反代之后(反代 IP 为私有)。直接公网直连时直连 IP 非私有,不走信任分支,不受影响。
**修复建议**(任一):
1. 反代配置覆盖而非追加:`proxy_set_header X-Forwarded-For $remote_addr`;
2. 代码改取 `XFF` **最后一个**值(追加模式下最后一个为真实来源);
3. 部署时用 `X-Real-IP` 且确保反代覆盖该头,代码优先信任 `X-Real-IP`。
---
## VUL-002 🟡 低 — transport 层错误不进入 UpstreamErrors 指标
**位置**: metrics/metrics.go:134-136、adapter/volcano/synthesis.go:90-92、112
**描述**: `UpstreamFinished` 中 `if errCode != 0 { UpstreamErrors.Inc(...) }`。传输错误(连接失败、DNS 失败、读流失败)时调用方传入的 `errCode` 均为 0:
- `client.PostStream` 失败 → `UpstreamFinished(..., 0)` → 不计
- `ParseStream` 读流错误 → `UpstreamError{Code: 0}` → 不计
而 `codeLabel`(metrics/metrics.go:148-158)明确设计了 `code == 0 → "transport"` 分类,**该分类永远不会被触发**。
**影响**: 上游网络故障时 `tts_upstream_errors_total` 不增长,`/metrics` 与监控面板无法发现"火山接口连不上"类故障,只能从日志人工发现。
**修复建议**: 将 transport 错误单独计数,例如 `if errCode != 0 || status == "transport_error" { UpstreamErrors.Inc(Labels{"code": codeLabel(errCode)}) }`。
---
## VUL-005 🟡 低 — 日志注入
**位置**: middleware/logger.go:26、adapter/volcano/synthesis.go:100(`Message` 拼入上游响应体)
**描述**: 访问日志直接拼接 `r.RequestURI`(客户端可控,URL 中可含 `\n`/`\r`);上游非 200 响应体 `rawBody` 拼入错误日志。Go `log` 不做转义,原样输出。
**影响**: 攻击者可在请求 URL 中注入换行符,伪造服务端日志行(如伪造"合成成功"记录、注入误导信息),干扰排障;无代码执行风险。
**修复建议**(低优先): 对 RequestURI 做换行转义(`strings.NewReplacer("\n", "\\n", "\r", "\\r")`)。
---
## VUL-006 🟡 低 — 监控端点无鉴权(设计权衡)
**位置**: router/router.go:21-31
**描述**: `/health`、`/metrics`、`/dashboard` 均不鉴权(README 明示,与 Prometheus 抓取场景对齐)。
**影响**: 若公网直接暴露,任何人可查看 `/metrics`(含 speaker/model/format 业务标签、请求计数、上游错误聚合)与 `/dashboard`(运行状态、配置检查结果)。不涉及凭据,但为侦察提供信息。
**判定**: 属于明确的设计决策,个人/内网使用可接受;公网部署建议通过反代鉴权(如 basic auth)保护 `/metrics`。
---
## VUL-007 🟡 低 — 未配置 OPENAI_TTS_API_KEY 时鉴权完全关闭
**位置**: middleware/auth.go:20-22、setting/config.go:64-79
**描述**: `ValidateAPIKey` 在 `setting.Auth.APIKeys` 为空时直接返回 `true`(全部放行)。该变量仅在 `OPENAI_TTS_API_KEY` 设置后才会填充。
**影响**: 公网直接暴露且未配置该环境变量时,任何人均可无限制调用 TTS 合成,消耗火山额度。
**判定**: 属设计行为(内网可信),README 已有说明。公网部署必须配置该变量,或由反代承担鉴权。
---
## VUL-008 ⚪ 信息 — speed 超范围静默截断
**位置**: adapter/volcano/request.go:89-101、controller/tts.go:132-141
**描述**: README 声明 `speed` 支持 0.25~4.0;controller 按此范围 clamp,但火山 `speech_rate` 仅支持 [-50, 100](即 0.5x~2.0x)。`0.25~0.5x` 与 `2.0~4.0x` 区间会被二次 clamp 截断,且无任何客户端提示。
**影响**: 用户请求 0.25x 实际得到 0.5x 语速,表现与预期不符。
**修复建议**: README 修正文档范围,或对超范围请求返回 400 而非静默截断。
---
## VUL-009 ⚪ 信息 — WAV 采样率依赖配置而非上游实际值
**位置**: adapter/volcano/audio.go:32-39、controller/tts.go:121
**描述**: `WrapWAVHeader` 使用 `opts.SampleRate`(环境变量 `BYTEDANCE_TTS_SAMPLE_RATE`,默认 24000)写 WAV 头。若上游实际返回的 PCM 采样率与配置不一致(配置错误或上游忽略该参数),WAV 头与数据不匹配。
**影响**: 音频以错误速率播放(变速/变调)。
**判定**: 正常配置下无影响;配置异常时表现为"音频怪声",README 第 3 条已有排查指引。
---
## 安全加固建议(非漏洞)
1. **Docker 环境变量**: compose 中密钥通过 `environment` 明文传递,进程环境可见(`/proc/<pid>/environ`)。生产可改用 Docker Secrets 或启动时注入。
2. **TLS**: 当前 HTTP 明文,建议生产经反代(nginx/caddy)终结 TLS,或服务前挂证书。
3. **依赖固定**: go.mod 仅锁定 `gorilla/mux v1.8.1`(2018 年发布),建议 `go get -u` 检查是否存在已知 CVE 的新版本,或至少 `go mod verify`。
## 工程债务(非安全,记录备查)
| 项目 | 说明 |
|---|---|
| 零单元测试 | 全部 12 个包无 `_test.go`;流解析(曾有 4 次 bug 修复)、WAV 拼头、speech_rate 转换、限流窗口、Prometheus 转义均无自动化回归保护 |
| 死代码 | `ratelimit_middleware.go` 整文件未引用;`auth.go:InitAPIKeys`、`cors.go:InitCORSConfig` 为未被调用的 no-op;`dto.ByteDanceTTSConfig` + `setting/config.go:311` 占位引用 |
| 冗余代码 | `resolveClientFormat`(controller/tts.go:49-52)两分支同值;`common.MaxResponseTimes`/`MaxErrors` 常量未使用 |
| 云盘占用 | 注释表明 `ratelimit_middleware.go` 因"云盘同步被永久占用"无法删除,仓库位于云盘目录,git 操作与文件删除存在异常风险 |
## 已核查无风险项
- ✅ API Key 比较使用 `subtle.ConstantTimeCompare`,无时序侧信道
- ✅ 启动日志与 `/health` 对 API Key 脱敏(`maskAPIKey`)
- ✅ 请求体上限 1MB、文本上限 5000 字、model 名长度/字符校验
- ✅ 并发信号量 + IP 限流仅对 `/v1/` 生效,监控路径豁免;CORS 预检不消耗配额
- ✅ telemetry label key 注册时锁定,当前 cardinality 可控(无客户端可控高基数标签)
- ✅ 上游连接池复用、超时(30s)与 context 取消正确传播
- ✅ 优雅退出(SIGINT/SIGTERM → 5s 内 Shutdown)
- ✅ 无 `_test.go` 之外的明显并发竞态:全局配置启动期写入后只读,共享状态均有锁