AppConfig与AppState调试输出密钥泄漏面 #148

Closed
opened 2026-08-07 17:00:40 +08:00 by lhk229 · 1 comment
Owner
[【安全报告】AppConfig与AppState调试输出密钥泄漏面-2026-08-07.md](/attachments/88270f17-4dcd-4cf1-9177-142b70b54ec1)
Author
Owner

【安全报告】AppConfig 与 AppState 调试输出密钥泄漏面

日期 2026-08-07
审计分支 feat/sound_opt(HEAD 69f850ff8
审计范围 server-rs/ 全部 crate(1500+ 个 .rs 文件),只读
定级 P2 — 潜在泄漏,未证实已发生
处置 不在 feat/sound_opt 修复;归属 master 独立安全维护

1. 结论摘要

api-server 的进程级配置对象 AppConfig 与运行时状态对象 AppState 都派生了 Debug,且没有任何脱敏。二者合计覆盖了本服务几乎全部密钥:JWT 签名密钥、后台管理员密码、内部服务 token、微信支付私钥与 APIv3 密钥、OSS AK/SK、SpacetimeDB token 与 bootstrap secret,以及全部 LLM / 图像 / 音频 provider 的 API key。

任何一处 format!("{state:?}")tracing::debug!(?state, ...) 都会把这些值一次性打进日志。

穷举搜索确认:当前生产代码中不存在这样的调用点,因此这是潜在泄漏而非已发生的泄漏。但当前的安全性来自"碰巧没人写过",而不是"写了也不会漏"——没有类型系统防线,没有工程规约,没有回归测试。触发成本是一行代码。

本报告在既有记录的基础上新增三项发现,其中第一项扩大了泄漏面:

  1. SpacetimeClient手写 Debug 实现本身就在泄漏 token,构成一条独立于 AppConfig 的第二路径;只修 AppConfig 修不掉它。
  2. 全仓 14 个 provider settings 类型中只有 2 个做了脱敏,且 3 处 Debug 哨兵测试全部只覆盖其中一个。
  3. #[instrument] 在全仓从未被使用,这一类常见的隐式泄漏机制在本仓库不存在。

2. 泄漏面:两条独立路径

路径一:AppConfig 直接派生 Debug

server-rs/crates/api-server/src/config.rs:31
    #[derive(Clone, Debug)]
    pub struct AppConfig {

全仓搜索 impl (std::fmt::)?Debug for (AppConfig|AppState|AppStateInner)零命中Debug 全部来自 derive,没有任何字段被隐去。

AppStateAppStateInner 逐层放大这条路径:

server-rs/crates/api-server/src/state.rs:126-127
    #[derive(Clone, Debug)]
    pub struct AppState(Arc<AppStateInner>);

server-rs/crates/api-server/src/state.rs:229-233
    #[derive(Debug)]
    pub struct AppStateInner {
        // 配置会在后续中间件、路由和平台适配接入时逐步消费。
        #[allow(dead_code)]
        pub config: AppConfig,

?state 会递归下钻到完整 AppConfig

路径二:SpacetimeClient 的手写 Debug(既有记录未覆盖)

这条路径独立于路径一,且更隐蔽——它出自一个特地手写、但没有做脱敏Debug 实现:

// server-rs/crates/spacetime-client/src/active.rs:982-993
impl fmt::Debug for SpacetimeClient {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("SpacetimeClient")
            .field("config", &self.config)          // ← 整个 config 原样塞入
            .field("pool_size", &self.pool.slots.len())
            .field(
                "cached_read_connection_enabled",
                &self.cached_read_connection.is_some(),
            )
            .finish()
    }
}

而被塞入的类型是普通 derive:

// server-rs/crates/spacetime-client/src/active.rs:109-117
#[derive(Clone, Debug)]
pub struct SpacetimeClientConfig {
    pub server_url: String,
    pub database: String,
    pub token: Option<String>,        // ← 明文
    pub pool_size: u32,
    pub procedure_timeout: Duration,
    pub subscribe_cached_read_models: bool,
}

tokenAppConfig.spacetime_tokenconfig.rs:167)灌入(state.rs:1747state.rs:1762),而 AppStateInner 同时持有 spacetime_client 字段。

结果:一次 ?state 会让 SpacetimeDB token 从两条独立路径各泄漏一次。 将来若只给 AppConfig 打脱敏补丁,这条路径依然畅通。

注:spacetime-client/src/lib.rs:1243-1254 有一份几乎相同的实现,但该 crate 的 Cargo.toml 指定 path = "src/active.rs"lib.rs 不在编译单元内,属孤立文件。


3. 敏感字段清单

AppConfigconfig.rs:33-216)中属密钥/凭证/签名材料的字段(按子系统归类,行号为当前工作区):

子系统 字段(行号)
JWT jwt_secret (91)
后台 admin_username (86)、admin_password (87)
内部服务 internal_api_secret (89)、bgfilter_internal_token (41)、editor_bgfilter_token (74)
微信 wechat_app_secret (120)、wechat_mini_program_app_secret (122)、wechat_pay_private_key_pem (140)、wechat_pay_private_key_path (141)、wechat_pay_api_v3_key (145)、wechat_mini_program_virtual_payment_app_key (150)、..._sandbox_app_key (151)、wechat_mini_program_message_token (154)、..._encoding_aes_key (155)
OSS oss_access_key_id (159)、oss_access_key_secret (160)
SpacetimeDB spacetime_token (167)、spacetime_runtime_service_bootstrap_secret (168)
LLM / provider llm_api_key (174)、vector_engine_api_key (190)、elevenlabs_api_key (194)
阿里云 aliyun_matting_access_key_id (78)、..._secret (79)、sms_access_key_id (102)、sms_access_key_secret (103)
内网地址 editor_bgfilter_base_url (73,默认值为硬编码内网 IP)

以上字段均无 #[serde(skip)]、无自定义 Debug、无包装类型。

elevenlabs_api_key(194)由提交 c3b3b47c9(SFX V2 T3,2026-08-07)引入。它进入的是一个已有二十余个同类字段处于同样状态的既有面,不是根因


4. 为什么目前是"潜在"而非"已发生"

五道门当前同时关着,逐条为穷举核查结果:

# 核查结论
1 直接格式化({config:?} / {state:?} / dbg! / println! 零命中
2 tracing 的 Debug 捕获(?config / ?state / config = ? 零命中
3 #[instrument](默认 Debug 捕获全部函数参数) 全仓从未使用过,机制未启用
4 间接嵌入(其它 derive-Debug 结构内嵌 AppConfig/AppState) 零命中。唯一命中的 PuzzleApiStatestate.rs:145-226)被 #[cfg(any())] 永久禁用;OpenAiImageSettings 内嵌 Option<AppState> 但手写 Debug 只打 .is_some()
5 其它出口(HTTP 端点回显 / Serialize / with_details / 环境变量转储 / 启动日志) 均无。AppConfigSerialize/healthz/readiness/api/speech/volcengine/config/api/runtime/frontend-config/admin/api/debug/http 逐一核过,返回的都是独立精简 DTO;启动日志只打 bind_address / worker_threads / otel_enabled 等开关摘要

此外还有一道配置层的门:默认日志级别是 info 而非 debug

// server-rs/crates/shared-logging/src/lib.rs:19-24
pub fn resolve_env_filter(default_filter: &str) -> EnvFilter {
    EnvFilter::try_from_default_env()
        .or_else(|_| EnvFilter::try_new(default_filter))
        .unwrap_or_else(|_| EnvFilter::new("info"))
}

api-server 默认 "info,tower_http=info"config.rs:341),可被 GENARRATIVE_API_LOG / RUST_LOG 覆盖。因此即便存在 debug!(?state),默认配置下事件会被 EnvFilter 丢弃,也不会进 OTel 管道(bridge 同样是 LevelFilter::INFO)。

这道门的边界必须说清楚:它是运行时配置而非代码约束。本次审计只覆盖仓库代码,未核查生产环境实际的 RUST_LOG / GENARRATIVE_API_LOG 取值。若要据此下"线上无泄漏"的结论,需要单独确认部署侧配置。

shared-logging 本身没有任何字段过滤或脱敏逻辑,只做级别过滤。


5. 现有防线的实际覆盖率

5.1 脱敏覆盖 2 / 14

全仓 provider / 配置类型的 Debug 现状:

类型 位置 脱敏
ElevenLabsAudioSettings platform-audio/src/elevenlabs.rs:28 api_key[redacted]
OpenAiImageSettings api-server/src/openai_image_generation.rs:50 api_key<redacted>,内嵌 Option<AppState> 只打 .is_some()
MattingClient platform-matting/src/lib.rs:286 ⚠️ 仅经 finish_non_exhaustive() 间接隐去;其 MattingConfig 自身仍是明文 derive
VectorEngineAudioSettings platform-audio/src/types.rs:83
VectorEngineImageSettings platform-image/src/vector_engine/types.rs:3
LlmConfig platform-llm/src/lib.rs:63
OssConfig platform-oss/src/lib.rs:71
WechatConfig platform-wechat/src/subscribe_message.rs:26
WechatPayConfig platform-wechat/src/pay.rs:97
RealWechatPayClient platform-wechat/src/pay.rs:121
Hyper3dSettings platform-hyper3d/src/types.rs:1
VolcengineSpeechConfig platform-speech/src/lib.rs:67
MattingConfig platform-matting/src/lib.rs:48
SpacetimeClientConfig spacetime-client/src/active.rs:109 (见 §2 路径二)
AppConfig api-server/src/config.rs:31

5.2 测试覆盖

仓库有成熟的"哨兵值"测试纪律(构造独特假密钥,断言输出不含它),共 12 处。但它们系统性地只覆盖**"HTTP 响应体 / error 字符串会不会回显上游明文"**这一类出口——微信支付回调、Prompt Assist 上游透传等链路覆盖密集。

针对 Debug 输出的哨兵测试只有 3 处,且全部只测 ElevenLabsAudioSettings

  • platform-audio/src/elevenlabs.rs:318 settings_debug_redacts_the_api_key
  • platform-audio/tests/elevenlabs.rs:305-306
  • api-server/src/vector_engine_audio_generation/settings.rs:115

没有任何一处覆盖 AppConfig / AppState / AppStateInner

5.3 无类型防线、无规约

  • 仓库未引入任何 secret wrapper:secrecy / zeroize / SecretString / Redacted<T> 在全部 39 个 crate 的 Cargo.toml 与源码中零命中(Cargo.lock 里的 zeroizering/rustls 的间接依赖,无直接使用)。
  • AGENTS.md 唯一沾边的是第 22 行"禁止提交 API Key、Token 到 git"——约束的是提交行为,不是运行时输出。全仓文档按"脱敏""日志.{0,10}密钥""不得记录"检索,没有覆盖运行时日志的通用规约。

因此,下一个写 debug!(?state, "...") 的人不会被任何机制拦下。


6. 风险性质

维度 评估
触发条件 一行代码。tracing::debug!(?state, "...")format!("{config:?}")
触发者 无需恶意;调试时顺手加一行诊断日志即可
影响范围 单次触发即泄露 JWT 签名密钥 + 后台密码 + 微信支付私钥 + OSS AK/SK + SpacetimeDB token + 全部 provider key
泄漏去向 日志文件、日志归档、OTel 管道——即扩散到多个留存系统
可发现性 低。泄漏后没有任何告警或断言会失败
当前状态 无已知触发点(穷举确认)

定级 P2 合理。但需要注意它的性质不是"影响有限",而是"目前未被触发"——一旦触发,后果是 P0 级。这类风险的正确处置是降低触发概率(脱敏 + 测试),而不是持续依赖代码审查发现 ?state


7. 修复建议(分级,供 master 侧排期)

第一档(低成本,覆盖主要面)

  1. AppConfig 手写 Debug:敏感字段打 <redacted>,非敏感字段照常输出。可直接照抄 OpenAiImageSettingsopenai_image_generation.rs:50-76)的写法——它已经示范了"内嵌对象只打 .is_some()、不递归下钻"。
  2. 同样处理 SpacetimeClientConfig(或改 SpacetimeClient 的手写 Debug,不再透传整个 config)。这一步不能省,否则 token 仍从路径二泄漏。
  3. 补哨兵测试:构造含唯一哨兵值的 AppConfig / AppState,断言 format!("{:?}") 不含任一哨兵。模板见 platform-audio/src/elevenlabs.rs:318

第二档(补齐覆盖)

对 §5.1 表中其余 11 个 类型逐个补脱敏 Debug,每个配一条哨兵测试。

第三档(根治,需独立排期)

引入 secret wrapper 类型(如 secrecy::SecretString),让"密钥字段不能被 Debug 打印"成为类型系统保证而非作者自觉。代价是要动十几个 crate 的字段类型与取值点,不适合与前两档合并。

配套:在 AGENTS.md 或 team-conventions 增补一条运行时日志规约("禁止对配置/状态对象做 Debug 格式化"),使其成为可被审查引用的显式条款。


8. 审计方法与可信度边界

已穷举验证(可作为否定性结论的依据):

  • impl Debug for (AppConfig|AppState|AppStateInner) 全仓搜索
  • {config:?} / {state:?} / {:?} 搭配 config/state 变量 / dbg! / println! / eprintln!
  • tracing 宏的 ? 前缀 Debug 捕获与 #[instrument]
  • 内嵌 AppConfig/AppState 且 derive Debug 的外层结构(递归两层)
  • panic / expect / assert! 自定义消息
  • 错误类型的 Debug / Display / thiserror 属性
  • impl Serialize for AppConfigenv::vars() 转储
  • secrecy / zeroize / secret wrapper 依赖与用法
  • impl ... Debug for 全仓手写实现(7 处,逐一判定是否脱敏)

已直接读取核对config.rs 字段区间、state.rsAppState/AppStateInner 定义、spacetime-client/src/active.rsSpacetimeClientConfigSpacetimeClient::fmtshared-logging/src/lib.rs 全文、各 provider settings 的 derive 行。

未覆盖,若要据此决策需另行确认

  1. 生产环境实际日志级别。本次只核查了代码默认值,未核查部署侧 RUST_LOG / GENARRATIVE_API_LOG 的真实取值。
  2. 历史日志归档。未核查已有日志中是否存在历史泄漏(例如某个已被删除的调试语句曾经打印过)。
  3. ring::signature::RsaKeyPair 的 Debug 行为RealWechatPayClient 持有该类型,其 Debug 是否泄漏私钥材料取决于 ring 自身实现,未在本仓库范围内验证。
  4. server-rs 的其它进程apps/ 下的 shell、miniprogram 等)不在本次范围内。

9. 与既有记录的关系

分支上已有 docs/project-memory/todos/【已知问题】AppConfig与AppState调试输出敏感配置泄漏-Master遗留-2026-08-07.md(提交 b64c3c05e,18 行)。本报告逐条核查后确认该记录的全部陈述成立,无矛盾。

本报告相对该记录的增量:§2 路径二(SpacetimeClient 手写 Debug 的独立泄漏路径)、§5.1 脱敏覆盖率清单、§5.2 测试覆盖缺口、§4 第 3 行(#[instrument] 未启用)、§8 可信度边界。

其中 §2 路径二会影响修复方案的正确性——按既有记录"为 AppConfigAppState 统一实现脱敏 Debug"的表述施工,若不同时处理 SpacetimeClientConfig,token 泄漏不会被修掉。

# 【安全报告】AppConfig 与 AppState 调试输出密钥泄漏面 | | | |---|---| | 日期 | 2026-08-07 | | 审计分支 | `feat/sound_opt`(HEAD `69f850ff8`) | | 审计范围 | `server-rs/` 全部 crate(1500+ 个 `.rs` 文件),只读 | | 定级 | **P2 — 潜在泄漏,未证实已发生** | | 处置 | 不在 `feat/sound_opt` 修复;归属 master 独立安全维护 | --- ## 1. 结论摘要 `api-server` 的进程级配置对象 `AppConfig` 与运行时状态对象 `AppState` 都派生了 `Debug`,且没有任何脱敏。二者合计覆盖了本服务几乎全部密钥:JWT 签名密钥、后台管理员密码、内部服务 token、微信支付私钥与 APIv3 密钥、OSS AK/SK、SpacetimeDB token 与 bootstrap secret,以及全部 LLM / 图像 / 音频 provider 的 API key。 **任何一处 `format!("{state:?}")` 或 `tracing::debug!(?state, ...)` 都会把这些值一次性打进日志。** 穷举搜索确认:当前生产代码中不存在这样的调用点,因此这是**潜在**泄漏而非已发生的泄漏。但当前的安全性来自"碰巧没人写过",而不是"写了也不会漏"——没有类型系统防线,没有工程规约,没有回归测试。触发成本是一行代码。 本报告在既有记录的基础上新增三项发现,其中第一项扩大了泄漏面: 1. `SpacetimeClient` 的**手写** `Debug` 实现本身就在泄漏 token,构成一条独立于 `AppConfig` 的第二路径;只修 `AppConfig` 修不掉它。 2. 全仓 14 个 provider settings 类型中只有 2 个做了脱敏,且 3 处 Debug 哨兵测试全部只覆盖其中一个。 3. `#[instrument]` 在全仓从未被使用,这一类常见的隐式泄漏机制在本仓库不存在。 --- ## 2. 泄漏面:两条独立路径 ### 路径一:`AppConfig` 直接派生 Debug ``` server-rs/crates/api-server/src/config.rs:31 #[derive(Clone, Debug)] pub struct AppConfig { ``` 全仓搜索 `impl (std::fmt::)?Debug for (AppConfig|AppState|AppStateInner)` — **零命中**。`Debug` 全部来自 derive,没有任何字段被隐去。 `AppState` 与 `AppStateInner` 逐层放大这条路径: ``` server-rs/crates/api-server/src/state.rs:126-127 #[derive(Clone, Debug)] pub struct AppState(Arc<AppStateInner>); server-rs/crates/api-server/src/state.rs:229-233 #[derive(Debug)] pub struct AppStateInner { // 配置会在后续中间件、路由和平台适配接入时逐步消费。 #[allow(dead_code)] pub config: AppConfig, ``` 即 `?state` 会递归下钻到完整 `AppConfig`。 ### 路径二:`SpacetimeClient` 的手写 Debug(既有记录未覆盖) 这条路径独立于路径一,且更隐蔽——它出自一个**特地手写、但没有做脱敏**的 `Debug` 实现: ```rust // server-rs/crates/spacetime-client/src/active.rs:982-993 impl fmt::Debug for SpacetimeClient { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("SpacetimeClient") .field("config", &self.config) // ← 整个 config 原样塞入 .field("pool_size", &self.pool.slots.len()) .field( "cached_read_connection_enabled", &self.cached_read_connection.is_some(), ) .finish() } } ``` 而被塞入的类型是普通 derive: ```rust // server-rs/crates/spacetime-client/src/active.rs:109-117 #[derive(Clone, Debug)] pub struct SpacetimeClientConfig { pub server_url: String, pub database: String, pub token: Option<String>, // ← 明文 pub pool_size: u32, pub procedure_timeout: Duration, pub subscribe_cached_read_models: bool, } ``` `token` 由 `AppConfig.spacetime_token`(`config.rs:167`)灌入(`state.rs:1747`、`state.rs:1762`),而 `AppStateInner` 同时持有 `spacetime_client` 字段。 **结果:一次 `?state` 会让 SpacetimeDB token 从两条独立路径各泄漏一次。** 将来若只给 `AppConfig` 打脱敏补丁,这条路径依然畅通。 注:`spacetime-client/src/lib.rs:1243-1254` 有一份几乎相同的实现,但该 crate 的 `Cargo.toml` 指定 `path = "src/active.rs"`,`lib.rs` 不在编译单元内,属孤立文件。 --- ## 3. 敏感字段清单 `AppConfig`(`config.rs:33-216`)中属密钥/凭证/签名材料的字段(按子系统归类,行号为当前工作区): | 子系统 | 字段(行号) | |---|---| | JWT | `jwt_secret` (91) | | 后台 | `admin_username` (86)、`admin_password` (87) | | 内部服务 | `internal_api_secret` (89)、`bgfilter_internal_token` (41)、`editor_bgfilter_token` (74) | | 微信 | `wechat_app_secret` (120)、`wechat_mini_program_app_secret` (122)、`wechat_pay_private_key_pem` (140)、`wechat_pay_private_key_path` (141)、`wechat_pay_api_v3_key` (145)、`wechat_mini_program_virtual_payment_app_key` (150)、`..._sandbox_app_key` (151)、`wechat_mini_program_message_token` (154)、`..._encoding_aes_key` (155) | | OSS | `oss_access_key_id` (159)、`oss_access_key_secret` (160) | | SpacetimeDB | `spacetime_token` (167)、`spacetime_runtime_service_bootstrap_secret` (168) | | LLM / provider | `llm_api_key` (174)、`vector_engine_api_key` (190)、`elevenlabs_api_key` (194) | | 阿里云 | `aliyun_matting_access_key_id` (78)、`..._secret` (79)、`sms_access_key_id` (102)、`sms_access_key_secret` (103) | | 内网地址 | `editor_bgfilter_base_url` (73,默认值为硬编码内网 IP) | 以上字段均无 `#[serde(skip)]`、无自定义 `Debug`、无包装类型。 `elevenlabs_api_key`(194)由提交 `c3b3b47c9`(SFX V2 T3,2026-08-07)引入。它进入的是一个已有二十余个同类字段处于同样状态的既有面,**不是根因**。 --- ## 4. 为什么目前是"潜在"而非"已发生" 五道门当前同时关着,逐条为穷举核查结果: | # | 门 | 核查结论 | |---|---|---| | 1 | 直接格式化(`{config:?}` / `{state:?}` / `dbg!` / `println!`) | 零命中 | | 2 | tracing 的 `Debug` 捕获(`?config` / `?state` / `config = ?`) | 零命中 | | 3 | `#[instrument]`(默认 Debug 捕获全部函数参数) | **全仓从未使用过**,机制未启用 | | 4 | 间接嵌入(其它 derive-Debug 结构内嵌 AppConfig/AppState) | 零命中。唯一命中的 `PuzzleApiState`(`state.rs:145-226`)被 `#[cfg(any())]` 永久禁用;`OpenAiImageSettings` 内嵌 `Option<AppState>` 但手写 Debug 只打 `.is_some()` | | 5 | 其它出口(HTTP 端点回显 / `Serialize` / `with_details` / 环境变量转储 / 启动日志) | 均无。`AppConfig` 无 `Serialize`;`/healthz`、`/readiness`、`/api/speech/volcengine/config`、`/api/runtime/frontend-config`、`/admin/api/debug/http` 逐一核过,返回的都是独立精简 DTO;启动日志只打 `bind_address` / `worker_threads` / `otel_enabled` 等开关摘要 | 此外还有一道**配置层**的门:默认日志级别是 `info` 而非 `debug`。 ```rust // server-rs/crates/shared-logging/src/lib.rs:19-24 pub fn resolve_env_filter(default_filter: &str) -> EnvFilter { EnvFilter::try_from_default_env() .or_else(|_| EnvFilter::try_new(default_filter)) .unwrap_or_else(|_| EnvFilter::new("info")) } ``` api-server 默认 `"info,tower_http=info"`(`config.rs:341`),可被 `GENARRATIVE_API_LOG` / `RUST_LOG` 覆盖。因此即便存在 `debug!(?state)`,默认配置下事件会被 `EnvFilter` 丢弃,也不会进 OTel 管道(bridge 同样是 `LevelFilter::INFO`)。 **这道门的边界必须说清楚**:它是运行时配置而非代码约束。本次审计只覆盖仓库代码,未核查生产环境实际的 `RUST_LOG` / `GENARRATIVE_API_LOG` 取值。若要据此下"线上无泄漏"的结论,需要单独确认部署侧配置。 `shared-logging` 本身**没有**任何字段过滤或脱敏逻辑,只做级别过滤。 --- ## 5. 现有防线的实际覆盖率 ### 5.1 脱敏覆盖 2 / 14 全仓 provider / 配置类型的 `Debug` 现状: | 类型 | 位置 | 脱敏 | |---|---|---| | `ElevenLabsAudioSettings` | `platform-audio/src/elevenlabs.rs:28` | ✅ `api_key` → `[redacted]` | | `OpenAiImageSettings` | `api-server/src/openai_image_generation.rs:50` | ✅ `api_key` → `<redacted>`,内嵌 `Option<AppState>` 只打 `.is_some()` | | `MattingClient` | `platform-matting/src/lib.rs:286` | ⚠️ 仅经 `finish_non_exhaustive()` 间接隐去;其 `MattingConfig` 自身仍是明文 derive | | `VectorEngineAudioSettings` | `platform-audio/src/types.rs:83` | ❌ | | `VectorEngineImageSettings` | `platform-image/src/vector_engine/types.rs:3` | ❌ | | `LlmConfig` | `platform-llm/src/lib.rs:63` | ❌ | | `OssConfig` | `platform-oss/src/lib.rs:71` | ❌ | | `WechatConfig` | `platform-wechat/src/subscribe_message.rs:26` | ❌ | | `WechatPayConfig` | `platform-wechat/src/pay.rs:97` | ❌ | | `RealWechatPayClient` | `platform-wechat/src/pay.rs:121` | ❌ | | `Hyper3dSettings` | `platform-hyper3d/src/types.rs:1` | ❌ | | `VolcengineSpeechConfig` | `platform-speech/src/lib.rs:67` | ❌ | | `MattingConfig` | `platform-matting/src/lib.rs:48` | ❌ | | `SpacetimeClientConfig` | `spacetime-client/src/active.rs:109` | ❌(见 §2 路径二) | | `AppConfig` | `api-server/src/config.rs:31` | ❌ | ### 5.2 测试覆盖 仓库有成熟的"哨兵值"测试纪律(构造独特假密钥,断言输出不含它),共 12 处。但它们系统性地只覆盖**"HTTP 响应体 / error 字符串会不会回显上游明文"**这一类出口——微信支付回调、Prompt Assist 上游透传等链路覆盖密集。 针对 **Debug 输出**的哨兵测试只有 3 处,且全部只测 `ElevenLabsAudioSettings`: - `platform-audio/src/elevenlabs.rs:318` `settings_debug_redacts_the_api_key` - `platform-audio/tests/elevenlabs.rs:305-306` - `api-server/src/vector_engine_audio_generation/settings.rs:115` **没有任何一处覆盖 `AppConfig` / `AppState` / `AppStateInner`。** ### 5.3 无类型防线、无规约 - 仓库未引入任何 secret wrapper:`secrecy` / `zeroize` / `SecretString` / `Redacted<T>` 在全部 39 个 crate 的 `Cargo.toml` 与源码中零命中(`Cargo.lock` 里的 `zeroize` 是 `ring`/`rustls` 的间接依赖,无直接使用)。 - AGENTS.md 唯一沾边的是第 22 行"禁止**提交** API Key、Token 到 git"——约束的是提交行为,不是运行时输出。全仓文档按"脱敏""日志.{0,10}密钥""不得记录"检索,**没有**覆盖运行时日志的通用规约。 **因此,下一个写 `debug!(?state, "...")` 的人不会被任何机制拦下。** --- ## 6. 风险性质 | 维度 | 评估 | |---|---| | 触发条件 | 一行代码。`tracing::debug!(?state, "...")` 或 `format!("{config:?}")` | | 触发者 | 无需恶意;调试时顺手加一行诊断日志即可 | | 影响范围 | 单次触发即泄露 JWT 签名密钥 + 后台密码 + 微信支付私钥 + OSS AK/SK + SpacetimeDB token + 全部 provider key | | 泄漏去向 | 日志文件、日志归档、OTel 管道——即扩散到多个留存系统 | | 可发现性 | 低。泄漏后没有任何告警或断言会失败 | | 当前状态 | 无已知触发点(穷举确认) | 定级 P2 合理。但需要注意它的性质不是"影响有限",而是"目前未被触发"——一旦触发,后果是 P0 级。这类风险的正确处置是**降低触发概率**(脱敏 + 测试),而不是持续依赖代码审查发现 `?state`。 --- ## 7. 修复建议(分级,供 master 侧排期) **第一档(低成本,覆盖主要面)** 1. 为 `AppConfig` 手写 `Debug`:敏感字段打 `<redacted>`,非敏感字段照常输出。可直接照抄 `OpenAiImageSettings`(`openai_image_generation.rs:50-76`)的写法——它已经示范了"内嵌对象只打 `.is_some()`、不递归下钻"。 2. 同样处理 `SpacetimeClientConfig`(或改 `SpacetimeClient` 的手写 Debug,不再透传整个 `config`)。**这一步不能省,否则 token 仍从路径二泄漏。** 3. 补哨兵测试:构造含唯一哨兵值的 `AppConfig` / `AppState`,断言 `format!("{:?}")` 不含任一哨兵。模板见 `platform-audio/src/elevenlabs.rs:318`。 **第二档(补齐覆盖)** 对 §5.1 表中其余 11 个 ❌ 类型逐个补脱敏 Debug,每个配一条哨兵测试。 **第三档(根治,需独立排期)** 引入 secret wrapper 类型(如 `secrecy::SecretString`),让"密钥字段不能被 Debug 打印"成为类型系统保证而非作者自觉。代价是要动十几个 crate 的字段类型与取值点,不适合与前两档合并。 **配套**:在 AGENTS.md 或 team-conventions 增补一条运行时日志规约("禁止对配置/状态对象做 Debug 格式化"),使其成为可被审查引用的显式条款。 --- ## 8. 审计方法与可信度边界 **已穷举验证**(可作为否定性结论的依据): - `impl Debug for (AppConfig|AppState|AppStateInner)` 全仓搜索 - `{config:?}` / `{state:?}` / `{:?}` 搭配 config/state 变量 / `dbg!` / `println!` / `eprintln!` - tracing 宏的 `?` 前缀 Debug 捕获与 `#[instrument]` - 内嵌 AppConfig/AppState 且 derive Debug 的外层结构(递归两层) - `panic` / `expect` / `assert!` 自定义消息 - 错误类型的 `Debug` / `Display` / `thiserror` 属性 - `impl Serialize for AppConfig`、`env::vars()` 转储 - `secrecy` / `zeroize` / secret wrapper 依赖与用法 - `impl ... Debug for` 全仓手写实现(7 处,逐一判定是否脱敏) **已直接读取核对**:`config.rs` 字段区间、`state.rs` 的 `AppState`/`AppStateInner` 定义、`spacetime-client/src/active.rs` 的 `SpacetimeClientConfig` 与 `SpacetimeClient::fmt`、`shared-logging/src/lib.rs` 全文、各 provider settings 的 derive 行。 **未覆盖,若要据此决策需另行确认**: 1. **生产环境实际日志级别**。本次只核查了代码默认值,未核查部署侧 `RUST_LOG` / `GENARRATIVE_API_LOG` 的真实取值。 2. **历史日志归档**。未核查已有日志中是否存在历史泄漏(例如某个已被删除的调试语句曾经打印过)。 3. **`ring::signature::RsaKeyPair` 的 Debug 行为**。`RealWechatPayClient` 持有该类型,其 Debug 是否泄漏私钥材料取决于 `ring` 自身实现,未在本仓库范围内验证。 4. **非 `server-rs` 的其它进程**(`apps/` 下的 shell、miniprogram 等)不在本次范围内。 --- ## 9. 与既有记录的关系 分支上已有 `docs/project-memory/todos/【已知问题】AppConfig与AppState调试输出敏感配置泄漏-Master遗留-2026-08-07.md`(提交 `b64c3c05e`,18 行)。本报告逐条核查后确认该记录的全部陈述成立,无矛盾。 本报告相对该记录的增量:§2 路径二(`SpacetimeClient` 手写 Debug 的独立泄漏路径)、§5.1 脱敏覆盖率清单、§5.2 测试覆盖缺口、§4 第 3 行(`#[instrument]` 未启用)、§8 可信度边界。 其中 **§2 路径二会影响修复方案的正确性**——按既有记录"为 `AppConfig`、`AppState` 统一实现脱敏 Debug"的表述施工,若不同时处理 `SpacetimeClientConfig`,token 泄漏不会被修掉。
kdletters added a new dependency 2026-08-07 19:22:52 +08:00
kdletters removed a dependency 2026-08-07 19:56:55 +08:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: GenarrativeAI/Genarrative#148