# 崩溃报告中的敏感数据：停止代理上下文泄露

崩溃报告可能会悄悄变成支持代理的应用中最宽泛的数据导出通道。团队会锁定生产凭据，却让异常 SDK 收集面包屑、HTTP 上下文、进程信息和复制的工具输入，然后将整个数据包发送到供应商账户。崩溃发生在本地，证据却没有留在本地。

解决办法不是放弃崩溃报告。代理集成出错时，普通日志无法解释问题，你仍然需要崩溃报告。关键在于确定哪些调试信息可以离开设备，默认停止收集原始请求材料，并证明收集器确实遵守这些限制。我见过一些事件复盘因此偏离重点：堆栈跟踪本身没有问题，但某个“有用”的上下文字段包含了完整的失败请求。

## 崩溃报告是证据包，不只是堆栈跟踪

崩溃报告通常是跨多个层级组装出来的数据包。操作系统可能生成原生诊断报告，应用可能附加异常元数据，崩溃 SDK 可能加入设备数据、面包屑、会话历史、日志、追踪字段和用户自定义标签。代理框架可能早已把提示词、工具输入、响应或重试状态序列化到这些字段之一。

这一点很重要，因为开发者往往只审查看得见的堆栈跟踪，然后认为结果干净。敏感材料通常就在旁边。

报告可能通过以下途径暴露数据：

- 将 URL、命令或响应体插入异常消息
- 由请求日志或工具调用事件生成的面包屑
- 接收完整请求对象的自定义字段
- 命令行参数、临时文件路径和由环境变量生成的值
- 附加文件、屏幕截图、会话回放或支持反馈

原生报告中的内存地址通常不会单独暴露 API 密钥。但复制到 panic 消息中的字符串确实可能暴露密钥。不要混淆这两种情况。原生诊断需要访问控制，应用遥测则需要有意识地减少数据。

Apple 关于诊断报告的文档将其描述为用于诊断应用问题的记录，其中包含进程和线程信息。这类信息很有用，通常也比应用监控事件更有限。但当 SDK 用你自己的运行时上下文丰富这份报告后，数据边界就发生了变化。应把这种增强后的事件视为即将发送到另一套系统的应用数据。

## 代理故障会产生异常丰富的诊断上下文

普通请求处理程序可能只会因路由和状态码而失败。代理操作则可能携带用户指令、代码库路径、命令输出、远程 API 参数、SSH 目标，以及导致故障的先前工具结果。这些上下文有助于复现问题，却也让粗心的采集代价更高。

风险路径通常很明确。开发者用一个便捷日志记录器包装每次工具调用，记录器输出完整的参数对象，可观测性 SDK 再把日志记录转换成面包屑。网络超时触发异常。此时报告已经包含工具输入，其中可能有标头或粘贴进去的令牌，供应商也会收到它。

下面这样的代码也会造成同样的问题：

```js
try {
  await runTool(toolName, input);
} catch (error) {
  throw new Error(`Tool failed: ${toolName} input=${JSON.stringify(input)} error=${error.message}`);
}
```

这条消息在本地测试时看起来很有帮助。在生产环境中，它会创建一个没有边界的数据字段，所有错误收集器、日志系统和告警集成都可能复制它。应改用稳定标识符和允许列表中的摘要：

```js
try {
  await runTool(toolName, input);
} catch (error) {
  throw new Error(`Tool failed: name=${toolName} request_id=${requestId} input_shape=${inputShape}`);
}
```

`inputShape` 可以表示允许的字段名称和大小列表，但绝不能包含字段值。如果你无法说明某个字段为什么属于错误事件，就不要放进去。复现问题应从指向受保护本地记录的关联 ID 开始，而不是把完整请求粘贴到托管仪表板中。

## 原始请求采集不应是默认设置

原始请求采集一直很流行，因为它能让第一次调试变得更快。但对于处理代理操作的服务来说，把它设为默认行为是错误的。授权标头、Cookie、签名 URL、查询参数、请求体和自定义标头都经常包含不应进入崩溃系统的内容。

安全事件需要包含足够的信息来进行分组、分诊和分派：

```json
{
  "request_id": "rq_8c2f1a",
  "channel": "http",
  "method": "POST",
  "route": "/v1/issues/{issue_id}/comments",
  "status_class": "5xx",
  "duration_ms": 8120,
  "attempt": 2,
  "error_kind": "upstream_timeout"
}
```

这里的路由使用模板，而不是字面路径。事件说明请求发生过重试，但没有说明它发送了什么。不透明 ID 让获得授权的响应人员可以在需要时检查本地源记录。

不要对机密进行哈希后就称其为已脱敏。确定性哈希仍然可以识别重复使用的 bearer 令牌；当原始值的搜索空间较小时，它还可能受到猜测攻击。应将敏感值替换为固定标记，或直接省略。如果你只需要知道是否存在凭据，可以记录 `auth_present: true`，而不要记录凭据类型、长度、前缀或指纹。

还要检查 URL 的处理方式。许多库会自动记录完整 URL。像 `/callback?code=...` 这样的路由，或签名下载 URL，可能通过开发者从未手动添加的字段泄露。应在事件进入 SDK 之前移除查询字符串，不要寄希望于后续处理器能识别所有变体。

## 脱敏必须在存储和导出之前执行

清理器是最后一道防线，不是收集一切的许可。不同 SDK 的钩子行为不同：有些处理最终事件，有些只处理选定字段，还有些无法覆盖原生崩溃附件或由独立集成生成的面包屑。只针对 `Authorization` 的规则可能漏掉 `authorization`、`x-api-token`、URL 参数，或嵌入异常中的 JSON 字符串。

应分层建立边界。首先关闭不需要的自动采集，然后只用允许列表中的字段构建事件，再对所有剩余字符串运行防御性清理器，最后测试收集器实际收到的序列化数据。

下面的伪代码展示了最能避免问题的顺序：

```text
request arrives
  -> derive route template and request ID
  -> retain protected local diagnostic record if policy permits
  -> create minimal crash context from allowlisted fields
  -> scrub all residual strings
  -> send minimized event
```

不要把原始请求对象传给“发送前”回调。库一旦检查过该对象，插件可能已经把它转换成面包屑或跨度。应给遥测代码传入一个小对象，从源头上确保它不可能包含敏感字段。

Sentry 介绍了数据清理和事件处理器，Firebase Crashlytics 介绍了自定义键和日志收集。应将这些设置理解为采集控制，而不是笼统的隐私保证。在这两种系统中，自定义上下文和日志都是应用团队最常破坏默认保护的位置。

## 集成变化后，SDK 默认行为也会变化

崩溃 SDK 升级后，可能新增追踪、控制台采集、面包屑、会话回放、性能监控，或新增能够看到更多请求状态的框架集成。应用只有堆栈跟踪时进行的安全审查，并不能覆盖新的数据路径。

为每个运行环境维护一份简短的采集清单。记录原生崩溃收集器、异常 SDK、日志桥接、追踪包、反馈组件和支持数据包机制。对每一项回答四个简单问题：什么会触发采集，它会自动收集哪些字段，上传前数据存在哪里，以及上传后谁能读取。

不要忘记旁路。开发者可能在崩溃产品中禁用了请求体，却把同一个异常转发给日志聚合器。告警规则可能会把异常文本复制到聊天通知中。支持按钮可能附加本地日志压缩包。你需要为整个事件路径绘制一张数据图，而不是为每个供应商分别编写一套过于乐观的说明。

在 macOS 上，同时检查应用级报告和操作系统诊断。Apple 崩溃报告可以保留在本地，也可以根据系统报告选项共享；第三方 SDK 则遵循自己的配置和网络路径。桌面应用应向操作人员清楚说明这一差异。“崩溃报告已禁用”没有意义，因为独立的错误监控客户端仍可能上传增强后的事件。

## 让凭据留在代理进程之外

脱敏只能减少代码处理机密后的暴露。更有效的做法是从一开始就避免让代理进程持有机密。这样，代理崩溃、环境复制和意外调试转储中可供捕获的敏感材料都会减少。

这并不意味着崩溃报告就无害了。代理仍可能持有私有源代码、提示词或工具参数。但如果进程从未收到 bearer 令牌，令牌就不可能从该进程泄露。

Sallyport 对 HTTP API 和 SSH 操作遵循这一边界：应用将凭据保存在加密保管库中，执行操作，并将结果返回给代理，而不是把凭据材料交给代理。这消除了崩溃遥测中一个反复出现的故障模式，但你仍需减少代理处理的请求数据和结果文本。

也不要把机密放进命令参数。进程参数出现在诊断工具中的频率往往高于开发者的预期，Shell 历史记录或进程检查也可能独立暴露它们。应通过受保护的代理或妥善管理的本地机制传递机密材料，不要使用 `--token=...` 这类可见参数。

## 本地保留也需要保留规则

对于难以诊断的故障，尤其是在受控开发环境中，将完整报告保存在机器上可能合适。但这并不自动安全。本地暂存区可能在事件结束后仍然存在，被复制进支持归档，或放在共享用户目录中，让其他进程能够读取。

应将两类记录分开。向远程系统发送用于分组和告警的最小化事件，在受保护的位置本地保留更完整的复现记录，并设置较短的保留期限和明确的删除路径。远程事件需要的是本地记录的不透明 ID，而不是记录内容。

有用的本地记录可以包含准确的构建标识符、经过清理的配置指纹、时间信息，以及对操作尝试的引用。但它不应是一个随意拼接环境变量、请求体和终端输出的文本文件。结构化本地存储让你可以应用与导出数据相同的允许列表和删除规则。

对于代理操作网关，审计记录需要特别谨慎。它们有助于确认代理尝试做了什么，但这不意味着可以把原始凭据或完整提示词复制到每个诊断子系统中。Sallyport 的审计跟踪将操作记录与代理本身分开，`sp audit verify` 命令可以在离线状态下验证加密哈希链，无需保管库密钥。验证可以告诉你记录是否被修改，但哪些诊断上下文适合发送到其他地方，仍取决于数据最小化原则。

## 用植入的机密验证收集器

配置审查可以发现明显错误，植入数据测试则能发现真正关键的遗漏。应在上线前、遥测 SDK 升级后，以及代理框架新增工具或日志集成时运行这类测试。

使用不会被误认为真实凭据的唯一标记字符串。将不同标记放入团队经常忘记检查的位置：授权标头、查询参数、JSON 请求体字段、代理指令、环境变量、命令参数和工具结果。触发受控异常，然后搜索每个目的地。

检查供应商事件视图、可用时的原始事件导出、本地崩溃暂存区、应用日志、追踪事件、告警载荷、支持附件，以及应用与收集器之间的任何队列。搜索确切标记，也搜索 URL 编码或转义 JSON 等常见变体。只检查网页仪表板，往往会漏掉面包屑或附加日志中的泄露。

记录预期结果。例如，报告可以包含 `route=/v1/files/{file_id}` 和 `request_id=rq_test_01`，但绝不能包含 `MARKER_HEADER_7`、`MARKER_PROMPT_7` 或字面查询字符串。将失败测试视为安全缺陷：关闭有问题的采集路径，添加回归测试，然后重新测试序列化事件。

## 简单审查就能阻止大多数意外导出

只要有人修改错误处理、可观测性、代理工具或支持诊断，就应审查崩溃遥测。审查人员应询问新代码是否可能序列化一个请求形状的对象，而不只是检查是否新增了某个秘密字段。

审查时可以使用这份简短清单：

- 错误消息包含标识符和类别，不包含序列化的输入或输出。
- 遥测只接收允许列表中的上下文，绝不接收原始请求或代理状态对象。
- URL 会移除查询字符串，标头不会进入面包屑或自定义字段。
- 完整本地诊断数据具有受保护的存储、删除规则，以及明确的存在理由。
- 植入标记测试覆盖每个已配置的导出目的地。

令人不安的是，良好的调试习惯往往正是泄露的原因。开发者会因为上次事件缺少上下文而添加更多信息。保留能够分类故障的上下文，在确有必要时将更完整的证据置于本地控制之下，不要仅仅因为崩溃 SDK 提供了一个字段，就把原始材料导出出去。
