# 审批通知预览：保护操作细节

审批通知预览需要像它所宣布的操作一样进行威胁建模。部署代码、调用客户 API 或运行远程命令的请求，可能在任何人按下「批准」之前就暴露敏感事实。如果这些事实出现在锁定的手机、共享桌面、会议室显示屏或镜像到可穿戴设备的界面上，审批系统就已经泄露了部分操作内容。

常见错误是把通知当成无害的基础设施。通知是一个拥有自身受众、保留行为和访问控制的输出渠道。应把它设计成一个有意限制内容的提示，引导用户进入受保护的决策界面。不要把它当成审批页面的缩小版。

## 锁屏是一道暴露边界

同事、家人、访客、摄像系统以及经过桌边的任何人，都可能看到锁屏。设备所有者也许就在附近，但附近不等于完成身份验证。这个区别听起来很明显，直到审批提醒显示出生产主机名、客户名称、事件标签或 shell 命令的第一行。

许多团队因为通知不包含秘密值，就把它归为低敏感度。这种判断范围太窄。像 `billing-prod.internal` 这样的端点、像 `/customers/28471/refund` 这样的路径，或像 `Rotate compromised access token` 这样的消息，都可能暴露系统、关系和运行状态。攻击者收集这些碎片后，不需要 bearer token 也能获得优势。

可以想想观察者从每个字段中推断出什么：

- 目标可能指明客户、地区、产品或生产服务。
- 操作可能透露某个账号正在被修改、退款正在等待处理，或某起事件正在进行。
- 代理身份可能暴露开发者正在处理哪个代码仓库或任务。
- 原因字段通常包含复制来的工单文本、用户输入或事件记录。
- 结果可能泄露操作在用户做出决定前检索到的数据。

锁屏只是第一道暴露边界。设备解锁后，操作系统可能仍会在通知中心显示相同文本。一个人离开共享工作站后，桌面通知也可能继续保留在历史记录中。智能手表可能镜像这条提醒。屏幕录制、远程支持软件和视频通话都可能捕获它。预览必须经得起所有这些场景。

Apple 的 Platform Security 文档把锁屏描述为受保护的设备状态，并将用户身份验证置于访问受保护数据的核心位置。但这个模型不会自动把通知文本变成受保护数据。应用必须自行决定发送给通知服务的内容，以及用户完成身份验证前要渲染的内容。把操作系统的隐私设置看作一层保护，而不是把敏感内容放进消息中的许可。

## 提醒应该请求关注，而不是泄露请求内容

安全的预览会告诉用户有一项操作需要审核，并提供足够的紧迫感，帮助用户安排优先级。它不会复述操作本身。团队经常混淆这两个概念：通知上下文不等于审批上下文。

审批上下文必须让操作人员能够做出有依据的决定。它可能需要确切目标、操作、凭据范围、代理进程、参数、预期影响和有效期。通知上下文的作用是把合适的人带回受保护的界面，所需信息少得多。

对于普通操作，锁屏预览可以这样写：

```text
Action approval needed
A development agent requests access to a protected service.
Expires in 4 minutes. Open the app to review.
```

这段文字说明了紧迫性和大致范围，但没有说明该服务处理的是工资、源代码、支付还是内部事件。它没有泄露 URL、方法、命令、分支、查询、客户身份或其他具体信息。

把它与真实系统中常见的消息比较一下：

```text
Approve POST https://payments.example.internal/v1/refunds/28471
Agent requests bearer token for customer refund. Reason: duplicate charge.
```

第二条提醒虽然没有打印 bearer token，却暴露了太多内容。它指出了敏感服务、具体操作、与客户有关的对象以及一笔财务事件。任何能读到预览的人，都获知了本无权知道的信息。

预览文案应使用一套简短词汇。「需要审批」「有访问请求等待处理」和「需要审核」通常已经足够。如果风险类别会影响用户响应的速度，可以添加粗略标签，例如「外部操作」「生产环境访问」或「敏感读取」。不要让标签具体到失去作用。「生产数据库导出」就不是粗略标签。

经过身份验证的界面则应当相反。它需要把请求讲清楚，让操作人员能够安全拒绝或有依据地批准。为了隐私而在那里隐藏细节，会造成盲目批准，只是换了一种失败方式。

## 通知离开应用前就必须完成删减

通知负载需要独立的结构。不要在最后一刻从完整审批记录中删减内容，也不要依赖字符串替换列表。团队之所以走这条捷径，是因为完整记录已经存在，渲染起来很方便。但一旦新增字段、URL 移到副标题，或原因字符串中包含复制来的敏感数据，这种做法就会失效。

应从操作请求中创建两个明确的投影。一个投影供经过身份验证的审批视图使用，另一个供预览使用。预览模型甚至不应该拥有原始 URL、请求头、命令参数、响应片段、秘密名称或自由文本原因这些字段。

下面的伪代码展示了这种结构：

```text
type ApprovalRecord {
  requestId
  agentAuthority
  destination
  operation
  arguments
  credentialReference
  userReason
  expiry
  riskClass
}

type NotificationPreview {
  requestId
  title
  body
  expiryText
  riskClass
}

function makePreview(record):
  return NotificationPreview(
    requestId = opaqueId(record.requestId),
    title = "Action approval needed",
    body = previewBody(record.riskClass),
    expiryText = formatExpiry(record.expiry),
    riskClass = record.riskClass
  )
```

关键不在措辞，而在数据的单向结构。`NotificationPreview` 根本无法意外包含 `destination`，因为这个字段并不存在。审查人员可以检查这道边界，测试也可以拒绝任何新增的、包含无界字符串的预览字段。

不要把原始原因文本交给清理器后就认为任务完成。原因中经常包含工单标题、粘贴的命令、电子邮件地址、账号标识符和内部名称。删减模式会漏掉没人预料到的格式。相比清理用户控制的句子，从枚举值中选择固定短语更安全。

不透明标识符也需要谨慎。像 `APR-10482` 这样的审批 ID 看起来可能无害，但可预测的编号会让观察者获得一个记录，并把它与可见工单或之后的对话关联起来。应使用没有业务含义、也不能充当授权令牌的标识符。更好的做法是，除非支持流程确实需要，否则不要把它放进预览。

把完整请求保存在加密的应用存储或其他经过身份验证的记录中，不要放在通知正文里。通知框架保存文本的时间可能比提醒本身可见的时间更长。如果另一个子系统保留了副本，应用的数据保留策略就无法覆盖它。

## 设备隐私设置有帮助，但不能承担全部设计

操作系统通常允许用户在设备锁定时隐藏通知预览。这个设置很有用，但审批产品不能假定它已启用、用户理解它，或它会在一个人的所有设备上保持一致。

有些人需要可见预览来筛选消息。有些组织会统一管理设置。有些设备没有锁屏密码。有些用户会在桌面已经解锁、同事正站在身后的情况下阅读提醒。应用如果发送敏感文本，然后说「用户可以关闭预览」，就把安全决策交给了系统配置中最不可靠的环节。

应针对三种情况进行设计：

1. 操作系统在锁定的显示屏上展示完整通知。
2. 操作系统隐藏正文，但显示标题或应用名称。
3. 显示屏已经解锁，但其他人仍然看得到。

第一种情况决定了你的负载内容。如果文本在这种情况下安全，另外两种情况就更容易分析。如果文本在这种情况下不安全，用户设置只会让问题变得时有时无。

不要过度依赖设备状态。应用可能知道自己的窗口已经解锁，却通常无法知道谁正在看通知。即使锁定状态信号可靠，也无法覆盖投影仪、外部显示器或屏幕共享。安全的预览在身份验证完成后仍应保持安全，因为物理观看条件不受应用控制。

有一个值得说明的例外：应用窗口中的本地、经过身份验证的通知，可以显示与审批视图相同的细节，因为应用已经控制了该窗口的访问权限。但这不是系统通知。不要因为两者都使用「通知」这个词，就把受保护的应用内收件箱和锁屏横幅混为一谈。

## 通知中的审批按钮会削弱决策边界

通知中的「批准」按钮看起来很高效，但它也容易导致误确认、被迫批准和上下文丢失。用户看到一条被截断的横幅，点下熟悉的按钮，却没有检查真正的目标或影响就授权了操作。

通知只能提供能够保留决策边界的操作。「打开审核」是安全的，因为它会把操作人员带到经过身份验证的应用中。「关闭」也可以是安全的，只要关闭不会拒绝、批准或悄悄延长请求。对于有实际影响的操作，即使操作系统会在执行前要求解锁设备，名为「批准」的按钮也不安全。

身份验证和知情同意是两项不同的检查。设备身份验证只能证明按下按钮的人能够解锁设备，不能证明他看到了完整请求，或有时间评估请求。审批页面应将决定绑定到请求细节，显示提醒出现后请求是否发生变化，并要求对敏感操作进行新的确认。

代理请求尤其需要注意这一点。代理可能连续生成许多从远处看起来相似的操作。「SSH 访问请求」这样的通知标题，无法区分只读状态检查和破坏性命令。操作人员释放操作前，受保护的界面必须展示具体意图。

Sallyport 的授权流程把真正的操作决定留在经过身份验证的应用中，而不是把操作系统通知变成控制代理访问的遥控器。这种设计没有一键审批那么显眼，但在请求具有实际影响时更加可靠。

通知中也不要提供「记住此选择」控件。持久权限变更需要独立且明确的界面、清晰的范围，以及查看或撤销权限的方式。锁屏上一次困倦的点击，不适合用来创建长期权限。

## 风险标签应该描述后果，而不是点名资产

过于笼统的提醒会让人养成打开每条通知的习惯。过于详细的提醒则会泄露受保护的资产。用后果类别而不是资源来描述风险，可以缓解这两种问题之间的冲突。

围绕操作能够产生的影响来建立类别。例如，操作可能读取受保护信息、修改内部服务、发送外部请求，或执行难以撤销的操作。这些类别能让审核者知道需要停下来处理，同时不泄露涉及哪个数据库、客户或主机名。

避免把访问敏感度和运行紧迫性混在一起。「高优先级」几乎无法说明批准后的影响。「生产环境写入」传递的信息更多，但仍然可能暴露正在使用生产系统。这样的措辞是否安全取决于环境。独自使用个人设备的人或许可以接受，共享的支持服务台则应使用更宽泛的标签。

以书面形式定义词汇，并在渲染通知前把它附加到操作上。如果开发者可以临时编写标题，世界上最严谨的结构也救不了你。一条简单的审查规则是：任何来自代理、用户、URL、命令、请求正文或远程响应的字符串，都不得出现在预览中。

实际映射可以是这样：

| 操作属性 | 预览措辞 | 受保护视图中的措辞 |
| --- | --- | --- |
| 读取受保护服务 | 敏感读取 | 确切服务、方法、路径、范围 |
| 修改内部状态 | 内部变更 | 目标、变更字段、预期影响 |
| 向组织外发送数据 | 外部操作 | 接收方、负载摘要、目标 |
| 可能难以撤销 | 高影响操作 | 完整命令或请求，以及恢复说明 |

这张表是供文案人员和工程师使用的策略，不代表每项操作都能完美归入四个盒子。如果一项操作有多种影响，应选择更严重的类别。一条过早请求关注的通知只会占用片刻时间，而把外部传输隐藏在「访问请求」下面，则可能导致错误决定。

## 通知历史和镜像设备需要单独审查

团队经常只测试第一条横幅就停下了。完整的暴露路径还包括保留的通知、通知摘要、可穿戴设备镜像、桌面转发，以及任何接收同一账号提醒的受管设备。

先使用一条包含明显测试数据的真实请求：虚构的客户名称、虚构的内部主机、虚构的电子邮件地址和虚构的命令参数。隐私测试不要使用真实生产值。触发请求后，检查文本可能出现的每个位置。

发布前使用以下测试顺序：

1. 锁定主设备并触发审批请求。
2. 检查横幅、锁屏列表，以及解锁后的通知中心。
3. 启用已配置的通知转发或可穿戴设备镜像，并检查其历史记录。
4. 通过团队使用的远程支持和屏幕共享路径捕获屏幕。
5. 让请求过期或完成处理，然后检查是否仍有旧文本可见。

在每个位置记录实际渲染出的标题和正文。成功的测试不是「我的手机隐藏了预览」，而是「测试标记没有出现在经过身份验证的应用之外」。这也能发现本地化问题。安全的英文模板在翻译后可能变长，把内部标识符挤到可见行中。

特别注意分组通知。系统可能显示最新消息、消息数量，或根据多条提醒拼出摘要。如果每条单独的预览都安全，分组结果也应该安全。如果分组代码使用「三条退款请求」这样的操作名称，就会通过旁路重新引入敏感细节。

通知持久化也会改变事件响应方式。撤销代理会话可以阻止未来的请求，但无法收回已经复制到用户通知历史或镜像设备中的文本。因此，预览最小化应该在授权和审计审查之前完成，而不是等到事件发生后再处理。

## 审计记录需要细节，预览需要克制

安全团队有时会因为审计记录本身不够详细，而把预览做得很笼统。他们担心详细记录会泄露信息，但这混淆了两个面向不同受众的系统。

审计记录应当足够完整，能够重建授权决定：哪个代理进程发起操作、它使用了什么权限、目标和操作是什么、发生时间、决定以及结果。敏感值仍然需要谨慎处理，但调查人员需要事实细节。除非用户已经在应用中完成身份验证，否则预览不应包含这些内容。

这种区别在故障发生时很重要。假设代理请求运行一条远程命令。提醒只显示「需要审批」，审核者打开应用后，受保护视图显示命令、主机、会话身份和有效期。审核者拒绝了请求。之后，工程师调查原因，需要找到对应记录。审计轨迹可以回答这个问题，不必让原始通知把命令带到每个显示界面。

Sallyport 将会话和活动日志与通知处理分开，并让两个日志都从加密、采用哈希链的审计日志中投影生成。这样，操作人员可以检查操作，并离线验证审计链，而不必把锁屏预览当成证据的替代品。

不要把审计哈希、记录摘录或原始关联 ID 放进通知。它们适合出现在受保护界面和调查工具中。在公共界面上，它们会生成观察者可以收集和关联的标识符。

底层决策过期时，通知也应过期，受保护记录应明确写出有效期。用户打开旧提醒时，应用必须获取当前请求状态。绝不要让缓存的通知正文诱导用户相信自己批准的仍是五分钟前存在的同一个请求。

## 把预览规则写成测试，然后设法击破它们

「避免敏感内容」这样的文字规范，在交付压力下很容易失效。应把它转化为随通知构建器运行的断言，也转化为审查人员可以阅读的测试用例。

最有用的测试不是脆弱的关键词黑名单，而是数据来源黑名单。拒绝任何来源于 URL、主机、命令文本、请求头、正文、凭据标签、自由文本原因、远程响应、电子邮件地址或账号标识符的预览字段。然后只允许一组范围有限的固定模板和有边界的类别值。

一个简洁的测试样例可以是：

```text
record.destination = "https://claims-prod.internal/cases/FAKE-784"
record.arguments = "--account fake.person@example.test --export"
record.userReason = "Customer Northstar reports a disputed claim"
record.riskClass = EXTERNAL_ACTION

preview = makePreview(record)

assert preview.title == "Action approval needed"
assert preview.body == "An external action requires review."
assert preview doesNotContain "claims"
assert preview doesNotContain "FAKE-784"
assert preview doesNotContain "fake.person"
assert preview doesNotContain "Northstar"
```

正向断言和负向断言同样重要。它们可以防止后续修改把安全的固定消息换成含糊的空白提醒，让用户养成直接忽略的习惯。还要测试有效期措辞、本地化、分组提醒和无障碍渲染。屏幕阅读器可能会读出视觉截断内容中隐藏的文字，因此无障碍输出也必须使用同一个受限的预览模型。

最后，请没有参与功能开发的人阅读这些预览，并让他寻找操作线索。他们会发现作者已经习以为常的内容：项目代号、熟悉的环境标签或内部服务昵称。如果一个了解情况的外部人员能够推断出受保护的操作，通知就包含了太多信息。

把默认预览设为在仍能及时获得人工响应的前提下最不具体的消息。真正审批所需的证据放在身份验证之后，并让每个快捷入口都回到这些证据。这样的设计可能多需要一次点击，却能避免每个锁屏都变成悄无声息的信息泄露渠道。
