# Basic 身份验证用户名属于敏感元数据

Basic 身份验证用户名属于敏感元数据。

只有在你需要收拾一次自动化运行留下的残局时，这句话才不会显得过于谨慎。比如，`billing-export@north-division` 被复制进 shell 记录、CI 日志、代理提示词和事件工单。四个地方都没有密码，但攻击者仍然知道 north-division 租户存在，知道它有账单导出集成，也能推测该账户可能连接到某个特定的旧版 API。

团队经常把数据分成秘密和其他所有内容。对于代理驱动的 API 工作，这种划分过于粗糙。用户名、租户 ID、主机名、API 路径和响应代码单独看似乎都没有问题，但组合起来就会描述出值得攻击的账户结构。尤其当代理可以比人更快、更远地复制上下文时，应把这些组合视为敏感元数据。

## 用户名可能暴露账户系统的构造方式

Basic 用户名通常比字段名称暗示的含义更多。旧版系统会把它用作登录名、客户代码、部门标签、服务角色、环境标记，或在更好的身份模型出现之前拼接出的复合标识符。

例如：

```text
acme-east:password
svc-payroll-prod:password
tenant-48291-export:password
j.smith@customer.example:password
```

第一个值告诉你客户和区域划分。第二个暴露了内部能力和环境。第三个透露租户标识符和功能。第四个暴露人员姓名以及客户关系。轮换密码只能修复密码这一部分，无法让已经复制出去的账户地图消失。

这很重要，因为攻击者并不会每次都从凭据开始攻击。他们会先建立目标清单。一个容易识别的用户名可以帮助攻击者编写可信的重置请求、猜测相关账户、搜索泄露数据、探测客户专属路径，或者利用听起来像内部信息的细节向支持人员施压。

不要因为用户名只有和密码一起才有效，就认为这个问题只是理论上的。知道 `svc-orders-import-prod` 存在，并不等于控制了它，但这仍然比一无所知有用得多。安全工作一旦等到某个字段组成完整凭据后才开始保护，成本就会迅速上升。

正确的分类取决于上下文。所有沙箱账户都共用的通用公开用户名可能不需要太多保护。与租户绑定的服务身份如果又配有内部端点，就需要更严格的处理。应为每个集成写清楚这一区别，而不是发生泄露后才套用一个笼统标签。

## Basic 身份验证会把账户名称附着在每次调用上

Basic 身份验证会将 `user-id:password` 值使用 Base64 编码，并放入 HTTP `Authorization` 请求头。Base64 只改变表示形式，无法对能够读取请求头的人隐藏原始字节。

典型请求如下：

```http
GET /v1/exports/monthly HTTP/1.1
Host: api.legacy.example
Authorization: Basic c3ZjLWJpbGxpbmctZXhwb3J0LXByb2Q6cmVkYWN0ZWQ=
Accept: application/json
```

每次客户端调用 API 时，编码后的文本都会携带用户名和密码。团队容易忘记的正是这种重复出现的特性。当然，bearer token 也可能暴露账户上下文，但许多 Basic 集成使用可读的用户名，解码后会直接写明这些信息。

RFC 7617 规定了这种方案，其中有两点与这里有关。第一处冒号会分隔 user-id 和密码，因此用户名不能包含冒号，控制字符也被禁止。这意味着旧版供应商可能接受一种看起来方便的账户命名方式，但该方式实际上无法通过符合标准的 Basic 请求头。不要自行发明转义规则，然后假设所有库都会遵循它。

第二点是字符处理。RFC 7617 允许服务器在质询中声明 UTF-8，但这只是提示，许多旧系统也无法稳定处理非 ASCII 身份。如果账户名称包含重音字符、大小写规则或经过转换的 Unicode，应在生产环境前测试确切的客户端和服务器组合。匹配失败可能变成身份验证错误，而有人可能为了“修复”它，把完整凭据大量写入调试输出。

Basic 身份验证还需要 HTTPS。OWASP 将 Basic 凭据描述为编码而非加密，并建议只要使用 Basic 就启用 TLS。TLS 能保护传输中的连接，却无法保护客户端库、反向代理、跟踪代理、错误处理器或调试工具记录下来的请求头。

## 租户 ID 和端点组合起来会变得危险

单独的租户 ID 可能只是一个不透明数字，单独的端点也可能只是通用路径。但把它们放在一起，就可能识别出客户的业务功能以及承载该功能的系统。

假设代理收到以下指令：

```text
For tenant 48291, call https://ledger.internal.example/v2/reconciliation/import
with username tenant-48291-ledger-import.
```

即使密码由另一个组件注入，这条指令也把租户、主机、操作和服务身份交给了代理。代理可能会在工作记录中引用它，工具包装器可能会记录它，模型会话可能会保留它，开发者还可能把错误复制到聊天频道。密码没有出现在明面上，但账户结构已经暴露。

当名称遵循可预测的语法时，暴露会进一步扩大。如果一个用户名是 `tenant-48291-ledger-import`，人们自然会猜测是否还存在 `tenant-48291-ledger-export`、`tenant-48291-reporting`，或其他租户对应的相同角色。可预测性方便运营人员，也方便枚举者。你不必放弃所有命名约定，但应认识到，这种约定可能让一个泄露的名称变成目录查询入口。

端点组合本身也会传递信号。即使主机名保持中性，`/admin/users`、`/payroll/export`、`/claims/submit` 和 `/archive/retention` 也会透露不同类型的工作。主机加路径再加租户信息，通常足以让钓鱼消息或冒充支持人员的请求显得可信。

应分类整个组合，而不只是其中的字段：

- 服务身份加租户 ID
- 服务身份加主机名
- 租户 ID 加端点路径
- 端点路径加响应正文或错误文本
- 时间戳加成功操作记录

这比“隐藏密码”更准确。它能告诉审查人员，为什么一条不含字面秘密的日志，也可能不适合发送到所有人都能访问的可观测系统。

## 代理会复制普通客户端不会复制的上下文

普通 API 客户端通常通过狭窄的配置路径接收 URL、账户名称和密码。AI 代理则把指令当作文本处理。它可能查看代码库、阅读工单、调用命令、解释错误并撰写摘要。每次交接都可能保留传统客户端根本不需要显示的标识符。

风险不在于代理天生粗心，而在于代理工作流本来就会让上下文变得可移动。代理能够跨部署说明和 API 响应进行推理，同样也会把租户名称和端点模式带入工具输入、会话记录或生成的补丁。

常见的失败过程如下：

1. 开发者把 Basic 用户名和租户代码放进本地 `.env` 文件，因为密码来自秘密存储。
2. 代理读取该文件，以了解失败的集成。
3. API 返回一条详细的 401 响应，其中重复了用户名和租户信息。
4. 代理生成故障排查报告，包含配置片段和错误。
5. 开发者把报告粘贴到更大范围的人都能看到的问题中。

没有人打算公开凭据，但工单现在记录了有效的命名模式、租户、主机、路径、服务角色，以及集成处于活动状态的时间范围。这些上下文足以制造后续风险。

不要通过禁止代理看到所有非秘密字符串来解决问题。这样会阻碍有用工作，而且通常无法真正执行。应改为确定每个操作需要哪些事实。一个需要请求月度导出的代理，也许只需要抽象操作名称和月份，不需要 Basic 用户名、密码、租户路由规则或原始目标 URL。

这会把设计问题从“代理能否完成身份验证”改成“代理为了生成目标请求，真正需要知道的最小操作描述是什么”。这个问题能帮助你把账户结构挡在代理上下文之外。

## 让身份材料远离提示词和代码库

提示词不适合充当配置存储。源文件、示例 curl 命令、问题模板、shell 别名和测试夹具也一样。它们都会比作者预想的范围传播得更远。

先把三件经常被混在一起的事分开：

1. **凭据材料**是用于身份验证的用户名和密码。
2. **路由元数据**告诉客户端请求发送到哪里，例如主机、租户分区或端点族。
3. **操作意图**是业务操作，例如“下载三月对账文件”。

人或代理通常可以表达操作意图，而不需要看到另外两类信息。这就是首选边界。如果旧版 API 强制通过租户专属主机或用户名进行路由，应把映射留在实际执行请求的组件中，而不是放在提出请求的提示词里。

不要在代码库中放入这样的原始 `.env` 示例：

```bash
LEGACY_API_URL=https://tenant-48291.api.legacy.example/v2/payroll/export
LEGACY_API_USER=tenant-48291-payroll-export
LEGACY_API_PASSWORD=replace-me
```

`replace-me` 占位符并不能让示例安全。URL 和用户名仍然记录了客户专属的集成模式。复制后的示例还可能在截止时间压力下被直接变成生产配置。

应使用抽象的本地契约：

```yaml
actions:
  export_monthly_payroll:
    account_ref: payroll-export-production
    target_ref: payroll-export-api
    inputs:
      - tenant_alias
      - month
```

`account_ref` 和 `target_ref` 应足够不透明，让代码库读者无法从中推断客户、主机名或服务角色。执行组件在本地解析它们。只有在确实需要时，代理才接收 `tenant_alias`，而当本地映射可以完成工作时，该别名不应使用生产租户标识符。

这种方法还可以避免一个常见的迁移错误：把密码移到秘密管理器，却把用户名、目标 URL 和客户代码继续硬编码在应用中。这比提交密码有所改进，但仍然会把结构暴露给每位开发者、每条构建日志和每次代码扫描导出。

## 使用不会把一次泄露变成目录的别名

不透明名称不是万能药，但可以减少意外披露带来的信息量。账户名称应向负责管理它的小范围人员说明用途，而不是向所有会看到它的系统宣传客户关系。

比较以下服务别名：

```text
bad:  acme-east-payroll-export-prod
better: svc-47f2-export-p1
```

第二个名称仍保留了易读的服务前缀和环境标记，这通常很实用，但有价值的细节到此为止。受保护的独立注册表可以把 `svc-47f2-export-p1` 映射到客户、负责人、端点、权限和轮换记录。

不要把看起来随机的别名当作授权的替代品。拥有密码的攻击者仍然可以进行身份验证，拥有注册表访问权限的员工仍然可以看到映射。别名减少不必要的披露，最小权限和访问审查才决定账户能够做什么。

有时供应商会规定用户名格式。某些旧版 API 要求客户编号、电子邮件地址或包含区域的复合值。遇到这种情况，应承认该字段敏感，并改变周围的处理方式。不要因为无法重命名它，就假装它是公开信息。

把供应商要求的标识符留在凭据边界内，让下游输出保持简单。请求执行器可以返回 `export accepted` 和任务 ID，不需要向调用代理重复提交的用户名、完整目标 URL 或租户路由值。

## 日志需要审计线索，但不需要账户目录

安全日志需要足够的信息来回答谁发起了操作、发生了什么、何时发生，以及是否成功。它不需要永久保存客户端发送的每个字节。OWASP 的日志指南明确警告不要记录密码、会话标识符和不必要的系统细节等敏感信息，同时也要求记录身份验证和访问控制事件。

这种矛盾确实存在。删掉所有内容，运营人员就无法调查事件。永久保留原始请求头和完整 URL，日志又会变成可搜索的账户目录。

可以使用区分运营关联信息和敏感元数据的记录结构：

```json
{
  "event": "legacy_api_call",
  "action": "export_monthly_payroll",
  "request_id": "req_01J...",
  "actor_run": "run_01J...",
  "credential_ref": "cred_4c91",
  "target_ref": "target_a77e",
  "result": "denied",
  "http_status": 401,
  "reason": "authentication_failed",
  "occurred_at": "2026-07-22T14:03:21Z"
}
```

这些引用可以让获授权的运营人员与受保护的清单进行关联，但不会把用户名、租户 ID、原始端点或 Authorization 请求头放进每条事件记录。清单的访问范围应小于普通日志的访问范围。

不要把用户名哈希后就认为问题解决了。用户名空间如果很小且有固定模式，确定性哈希通常很容易猜出，而且稳定哈希仍然允许跨记录追踪账户。如果需要关联，请使用由自身系统分配的随机凭据引用。轮换凭据时，也应轮换或停用该引用。

要谨慎处理失败诊断。以下是不应直接传递的响应：

```text
401 for tenant-48291-payroll-export at /v2/payroll/export: user exists but password rejected
```

它确认了账户、租户、路径和验证结果。更好的内部处理方式是：如果确实需要，把供应商的详细诊断放进受限支持记录；普通活动记录只写入 `authentication_failed`。代理应收到一条简短失败信息，说明需要停止并寻求帮助，而不是得到更多尝试变体的线索。

## 脱敏和最小化解决的是不同问题

脱敏会在某人已经处理某个危险值之后移除它。最小化则是在值进入不该进入的地方之前阻止它。两者都需要，混淆二者会产生糟糕的设计。

请求头清理器可以把：

```http
Authorization: Basic c3ZjLWJpbGxpbmctZXhwb3J0LXByb2Q6cmVkYWN0ZWQ=
```

替换为：

```http
Authorization: [REDACTED]
```

这是必要的，但它无法处理路径、主机名、租户查询参数、详细的 401 正文、请求标签或记录在请求头旁边的跟踪属性。一个自豪地隐藏密码、却保留 `tenant-48291.api.legacy.example/payroll/export` 的系统，只降低了一种风险，却留下了一张有用的账户地图。

最小化需要在请求运行前提出更难的问题：

- 代理需要实际目标主机，还是只需要操作名称？
- 活动日志需要租户标识符，还是只需要受保护的引用？
- 支持人员是否需要在普通日志流中看到供应商原始响应？
- 执行器可以在本地解析租户时，请求是否还需要把租户放进 URL？
- 审批人员需要看到完整身份，还是只需要一个便于人理解、但不会暴露信息的标签？

许多团队会在这里提出一个流行但错误的建议：“完整记录一次请求，之后再改进过滤器。”他们这样说，是因为调试旧版 API 很痛苦，而完整捕获可以快速回答问题。但捕获的数据随后会永久存在于备份、分析存储、测试环境和复制的事件记录中。应为短期调查建立受限诊断路径，不要把广泛的原始捕获作为正常运行模式。

## 旧版 API 需要隔离边界，而不是盲目信任

你可能无法在本季度替换 Basic 身份验证。供应商可能只支持一种集成方式，仓储设备也可能使用多年前就冻结的 API。实际做法是隔离，而不是一厢情愿地宣布旧版 API 可以接受。

把 Basic 凭据和标识符映射放在一个自身执行 HTTP 请求的组件后面。代理应请求带有结构化输入的命名操作。请求组件选择目标、取得凭据、把凭据注入请求头、检查预期范围，然后返回有限结果。

代理操作可以使用这样的请求契约：

```json
{
  "action": "export_monthly_payroll",
  "tenant_alias": "tenant_ref_91ab",
  "month": "2026-06"
}
```

操作执行器可以验证月份，在受保护的本地映射中解析 `tenant_ref_91ab`，然后调用供应商。它应拒绝 `url`、`authorization`、`username` 和 `headers` 等额外字段。如果调用方可以覆盖这些字段，就能把受信任的凭据发往任意主机，或把一个范围有限的操作重新变成原始 HTTP 客户端。

这项限制不仅关系到凭据暴露，也关系到 SSRF 类错误。当执行器持有凭据时，用户提供的 URL 绝不是无害的便利功能。目标必须来自受控定义，重定向也需要同样谨慎处理。不要在保留 Authorization 请求头的同时跟随重定向到新的来源。

这个边界还应负责处理重试。设计不佳的代理可能会在 401 后改变输入并重试，或在超时后反复调用操作。执行器应区分安全的传输重试、身份验证失败和未知的写入结果。对于不可幂等的旧版端点，应返回要求人工审查的状态，而不是因为代理自信地提出请求就再次发送相同操作。

当支持 MCP 的代理需要调用 HTTP API、但不应接收 Basic 凭据时，Sallyport 适合这种模式。它的 HTTP 通道会在应用内部注入 Basic 凭据，而每个会话和每次调用的控制功能可以让人员决定代理运行何时能够使用该账户。代理得到的是操作结果，而不是可以带到其他地方重复使用的明文用户名或密码。

## 审批界面应提供足够上下文，以便发现错误操作

只写着“允许 API 调用吗？”的审批对话框没有帮助。打印完整 Basic 用户名、租户、完整 URL 和原始请求正文的对话框又分享了过多信息。审查人员需要一段简洁描述，能够发现错误操作，同时不暴露整个账户结构。

显示操作名称、受保护的目标标签、为审查人员选择的租户显示标签、操作类型和高层次后果。例如：

```text
Allow export_monthly_payroll?
Target: Payroll export service
Tenant: Finance tenant 7
Operation: Create June 2026 export
Agent run: signed local coding process
```

这样审查人员可以发现异常月份、功能或目标，但审批记录不会暴露供应商主机名、租户编号或服务用户名。

显示标签需要治理。如果“Finance tenant 7”所在团队只有一个金融客户，它仍然可能识别该客户。标签应适合看到它的群体。安全并不是把每个有用名称都替换成让审查人员无法理解的不透明代码。

对于敏感的 Basic 账户，如果操作可能转移资金、导出受监管记录、改变访问权限或联系外部实体，应每次使用都要求批准。对于一次会执行大量低风险读取的短期运行，按会话批准更实用。选择应依据账户权限和操作后果，而不是因为 Basic 身份验证已经过时。

保留审批决定和后续调用的可审计记录。只有当防篡改记录描述了主体、操作、结果和审批状态，同时没有变成凭据地图的另一份副本时，它才真正有用。Sallyport 的独立会话日志和活动日志正是围绕这种区分设计的，两种视图都来自其加密哈希链审计日志。

## 从一开始就让账户结构难以被收集

解决方案不是一份写着“谨慎处理元数据”的巨大政策文件，而是让开发者和代理自然使用更安全的路径。

盘点每个 Basic 集成，并逐一回答：用户名透露了什么？它是否包含人员、客户、环境、产品或角色？哪个主机和端点组合会让这个值更容易暴露？这些值现在出现在哪里？真正需要看到它们的是谁？

然后移除容易复制的内容。用操作示例替换原始 curl 片段，用中性的测试别名替换租户专属夹具名称。在应用日志中阻止 Authorization 请求头和 URL userinfo。让代理可见的输出不包含完整供应商诊断。在凭据边界拒绝用户提供的请求目标。为支持人员提供受控的详细信息检索方式，仅在事件确实需要时使用。

RFC 9110 规定，发送方不得生成包含 `user:password@host` userinfo 的 HTTP 或 HTTPS URI 引用，并警告实现可能会在配置或命令选项中使用这种形式时暴露用户标识符或密码。这一警告也适用于 URL 之外的场景：放进方便文本字段的身份信息，很容易被复制到原本不该出现的地方。

Basic 身份验证可能会因为供应商没有迁移而继续存在，但你自己的处理方式不必停留在过去。当你把用户名、租户 ID 和端点组合视为敏感元数据，就不会再把账户目录和任务一起交给代理。
