# AI 代理文件上传需要明确的硬边界

上传端点就是一个出站数据通道，即使工程师把它称为附件功能。一旦 AI 代理可以附加日志、导出文件、屏幕截图或客户文件，一句含糊的指令，例如「把这个发给支持团队」，就可能变成无法挽回的数据泄露。

AI 代理文件上传需要对字节、内容和接收方设置明确的硬边界。应把这些边界放进执行上传的操作中，而不是写在代理提示词里，也不要依赖审查人员在忙碌一天结束时发现一个不良文件名。

## 上传操作必须声明哪些内容可以离开系统

安全的上传流程要先把每个文件视为一个用途明确的对象。用途决定最大大小、允许格式、允许的接收方、保留要求，以及发送前是否必须由人审批。如果 API 接受任意 multipart 请求体和调用方提供的 URL，你就创建了一条通用的数据外泄通道，只是开发者接口做得很友好。

人们常混淆两件事：从不可信客户端接收文件，以及代表代理发送文件。传统的上传指南重点是保护服务器不受恶意文件攻击。代理上传同样需要这些保护，但更直接的风险是防止数据被过度发送。某个 PDF 可能非常适合解析，却完全不适合交给工单系统。

不要定义通用的 `upload_file` 操作，而应定义有名称的上传用途。例如，`diagnostic_bundle` 可以允许将压缩的支持包发送给一个支持系统接收方。`invoice_export` 可以允许将 CSV 发送给财务接收方。这两种用途都不应在同一个请求中同时接受路径、接收方 URL 和任意文件。

一个小型契约就能让边界清晰可见：

```json
{
  "purpose": "diagnostic_bundle",
  "file_path": "/private/tmp/app-diagnostics-2025-03-08.zip",
  "destination_id": "support-case",
  "case_reference": "CASE-1842"
}
```

调用方选择已批准的用途，并提供业务操作所需的元数据。上传服务将 `support-case` 映射到自己管理的接收方，不允许调用方用问题评论、文档或工具响应中嵌入的 URL 替换接收方。

文件路径也要受到控制。代理只能从指定的暂存目录中选择文件，或者由服务根据已知输入自行生成附件。允许代理指定任意可读路径，就等于允许它读取配置文件、SSH 材料、浏览器数据或其他用户的导出文件。

## 大小限制需要测量两个维度

文件限制应在服务存储、扫描或转发之前拒绝过大的请求体。在 HTTP 边缘层，如果存在 `Content-Length`，先用它执行限制，然后在读取过程中继续统计字节数，因为客户端可能省略或伪造这个标头。

限制必须符合用途。客户 CSV 的 25 MB 限制，与压缩诊断包的 25 MB 限制，并不是同一个决定。解析器读取 CSV 时，文件可能扩展为更大的内存分配。压缩包解压后也可能比传输大小大很多倍。因此应同时设置传输大小限制和处理后大小限制。

不要让代理把被拒绝的文件拆成许多合法请求，除非接收方明确支持分块上传，并且你的服务会跟踪总大小。否则，10 MB 规则会变成 100 个分片的传输，原本以为拥有的保护全部失效。

尽早拒绝，并返回能让代理安全恢复的响应：

```json
{
  "error": "attachment_too_large",
  "purpose": "diagnostic_bundle",
  "observed_bytes": 12582911,
  "max_bytes": 8388608,
  "safe_alternatives": [
    "create_redacted_diagnostic_bundle",
    "attach_selected_log_window"
  ]
}
```

这个响应很重要。如果只返回「上传失败」，代理可能会尝试另一个目标、压缩文件，或持续重试。应告诉它下一步允许执行什么操作，同时不要通过调试消息泄露被拒绝的文件。

缓冲也是一个容易被忽略的失败点。许多框架会在应用代码看到大小之前，先把 multipart 请求解析到内存或临时目录中。应让 Web 服务器、框架解析器、反向代理和应用读取器使用一致的限制。最小限制最终会生效，但某个意外更大的层仍可能在较小的层拒绝请求前消耗磁盘空间。

还要测量文本标准化、图像转换、文档提取和压缩包解包后的大小。只限制原始附件，并不能控制后续生成材料的资源占用或泄露风险。

## 文件名和 MIME 标头几乎不能证明什么

文件类型检查需要独立证据，因为文件扩展名和 `Content-Type` 标头都来自发送方。代理可能并无恶意，只是转发了误导性的标签。支持工具也可能把每个附件都标为 `application/octet-stream`。无论哪种情况，接收方都应根据字节内容和允许的结构作出判断。

OWASP 的 File Upload Cheat Sheet 建议使用扩展名允许列表，不信任 `Content-Type` 标头，由服务器生成文件名，并将上传内容存放在 Web 根目录之外。这些建议依然有效，但代理工作流还需要增加一条规则：在服务联系远程接收方之前，先根据已命名的用途验证文件类型。有效的 PDF 并不自动适用于所有上传用途。

可以使用多种检查，分别回答不同问题：

- 扩展名告诉你发送方声称文件是什么。
- 签名字节告诉你内容开头是否符合声称的格式。
- 有边界的解析器告诉你这些字节是否足够符合格式，可以安全处理。
- 内容检查告诉你文件是否包含该用途禁止的材料。

对于 CSV 导出，可以只允许 `.csv` 和 UTF-8 文本，解析有限的样本，并拒绝嵌入的二进制数据或异常宽的行。对于 PDF，应验证 `%PDF-` 签名，应用大小限制；如果必须检查页面，就使用有时间和内存限制的解析器。对于图像，应在处理前解码尺寸。文件大小适中的图像，解码后仍可能分配过多内存。

避免笼统接受「压缩包」类型。ZIP、TAR 和 GZIP 各不相同，也都有自己的检查工作。如果业务流程不需要压缩包，就拒绝它。因为用户偶尔会需要某种格式而接受它，正是通用附件端点出现的原因。

在服务端重命名已接受的文件。清理原始文件名后，可以把它作为显示元数据保留，但不要未经转义就将它用作文件系统路径、对象存储键或 `Content-Disposition` 值。文件名可能包含控制字符、具有欺骗性的 Unicode、路径分隔符，以及会改变下游日志的字符串。

## 目标检查必须经得住重定向

接收方允许列表必须准确标识可以接收文件的地点。允许 `https://example.com` 还不够，因为 HTTP 客户端可能跟随重定向到另一台主机、解析到内部地址，或接受不同的端口。

将目标保存为服务器端记录，固定协议、主机、端口、路径前缀、凭据身份和允许的用途。操作只接收 `destination_id`，绝不接收自由填写的端点。如果接收方要求在路径中加入案件 ID，应根据受约束的标识符构建路径，而不是从代理那里接收完整 URL。

每次发送时，HTTP 客户端都应执行以下检查：

1. 除非有记录在案的内部例外，否则必须使用 HTTPS。
2. 建立连接前，将请求的主机和端口与目标记录进行匹配。
3. 默认禁用重定向。如果接收方确实需要重定向，则在发送下一个字节前，按照同一条目标记录验证每个重定向目标。
4. 拒绝 IP 字面量、回环地址、链路本地地址和私有地址范围，除非该目标明确对应受控的内部服务。
5. 固定允许的路径前缀和 HTTP 方法，不要允许访问整个主机。

第四点通常被称为 SSRF 防护，确实如此。它还可以防止代理跟随任务描述中的 URL，造成意外泄露。能够访问内部网络的上传客户端，绝不能把任务文本当作联系某个地址的授权依据。

不要转发原始授权上下文。用于向案件管理 API 发送请求的凭据只能属于该接收方和该操作。会复制任意代理标头的文件上传网关，会更容易受到标头注入，也会让代理能够间接选择凭据。

记录重定向后的最终 URL，但要从运行日志中删去查询参数值。查询字符串经常包含签名上传令牌。记录目标很有用，但把可直接使用的授权令牌写入日志是不负责任的。

## 日志需要一条有意设计的导出路径

日志是团队最容易低估的附件类别。它们可能包含请求标头、客户标识符、SQL 片段、堆栈跟踪、内部主机名，有时还包括完整的请求或响应正文。日志位于开发者目录中，并不意味着可以安全地通过电子邮件发送或上传。

不要靠一条「删除机密」的指令解决问题。代理可能漏掉某些格式，过度遮盖内容，或把可疑字符串判断为无害上下文。应创建诊断包生成器，读取已知文件，应用确定性的过滤器，并在暂存目录中生成新的工件。

实用的过滤策略可以删除完整字段，而不是试图找出所有可能的机密模式。默认删除 `Authorization`、`Cookie`、`Set-Cookie`、API 密钥字段、会话标识符和请求正文。当需要关联记录时，用稳定的本地令牌替换客户标识符。将时间范围限制在事故窗口内，不要发送数周的历史记录。

例如，与原始 HTTP 跟踪相比，下面这种诊断记录更安全：

```json
{
  "time": "2025-03-08T14:22:11Z",
  "request_id": "local-7f3c",
  "method": "POST",
  "route": "/v1/reports",
  "status": 502,
  "upstream": "reporting-service",
  "authorization": "[removed]",
  "body": "[omitted]"
}
```

包生成器应生成清单，列出文件名、字节数、哈希值和已应用的过滤器。这样审查人员无需打开每个附件，也能检查具体内容。接收方也能据此识别被截断或遭修改的上传内容。

数据库导出需要更严格的规则。不能因为工单写着「发送一个样本」，就让代理附加生产环境导出文件。应生成仅包含架构的工件、只查询获准列的专用查询结果，或生成合成复现数据。如果事故确实需要真实客户记录，应将其作为独立操作，明确目标、范围，并要求人工审批。

屏幕截图也应同样谨慎。它们可能包含客户账户、消息、浏览器标签页、通知和本地路径。与其把一个宽泛的截图目录交给代理，不如通过受控工具裁剪截图，或生成范围明确的截图。

## 压缩包和办公文档隐藏的不止一个文件

压缩包会在一个上传文件中创建第二组文件。发送前应检查成员列表，并限制成员数量、压缩字节数、解压字节数、路径深度和嵌套层级。拒绝使用绝对路径、`..` 路径遍历、重复名称或符号链接的条目。

一个磁盘上只有 2 MB 的 ZIP 文件，解压后可能膨胀到耗尽工作进程或接收方资源的大小。这通常被称为解压炸弹，但问题并不只来自恶意输入。构建系统可能意外生成巨大的压缩包，代理也可能附加第一个看起来像导出文件的内容。

办公文档同样需要谨慎处理。现代文档格式通常包含 ZIP 容器、嵌入媒体、元数据、评论、修订记录和外部关系。文档页面看起来已经删减内容，但其内部包仍可能保留旧文本或作者信息。如果工作流只需要渲染后的内容，应根据获准数据生成新的 PDF，而不是转发可编辑的原始文档。

不要在请求路径中递归「清理」任意文档。解析和重写复杂格式本身就有安全性和可靠性成本。对于范围明确的工作流，应只接受范围明确的生成工件。对于例外文档，应将其交给人工审查流程，由人检查实际文件和目标。

## 操作契约应让不安全的请求无法执行

面向代理的工具应暴露与你的控制措施相匹配的选项，而不是一个原始 HTTP 请求构造器。如果工具提供 `url`、`headers`、`file_path` 和 `method`，策略就已经失去了大部分约束。

应使用这样的请求模式：不可信字段描述意图，可信记录提供权限。下面的例子把接收方、凭据和允许的文件类别都置于代理控制之外：

```json
{
  "action": "send_attachment",
  "purpose": "customer_export",
  "destination_id": "finance-import",
  "artifact_id": "exp_8c4e1a",
  "note": "March reconciliation correction"
}
```

服务将 `artifact_id` 解析为由自己创建，或通过独立接收流程接受的暂存对象。服务自行计算摘要并检测类型，将 `destination_id` 解析为固定的接收方记录，并在发送时加入接收方凭据。代理永远看不到该凭据，也不能在审批后替换另一个接收方。

预检结果应暴露足够的信息，让人能够作出知情决定：

```json
{
  "decision": "approval_required",
  "artifact": {
    "name": "reconciliation-2025-03.csv",
    "bytes": 482913,
    "detected_type": "text/csv",
    "sha256": "a4d1...c09e"
  },
  "destination": {
    "label": "Finance import",
    "host": "imports.example.internal",
    "path": "/v2/reconciliation"
  },
  "reason": "customer_export requires approval"
}
```

不要只给审查人员显示文件名和批准按钮。应显示测得的大小、检测到的类型、目标主机、目标标签和用途。如果文件敏感，应在审批界面显示抽样分类结果或清单，而不是完整文件内容。

让工件 ID 短期有效且只对应一个用途。暂存的诊断包不应在支持案件关闭后，仍能作为通用上传对象重复使用。创建工件时就将它绑定到用途和接收方，并在较短的操作窗口后使其失效。

重试需要幂等性。网络故障很常见，代理也会频繁重试。应在操作层生成幂等令牌，将它绑定到工件摘要和目标，并且只为同一次预期发送重复使用。带着更改后的路径或摘要重试时，不得继承之前的授权。

## 人工审批只有在标记真实边界时才有用

审批不能替代验证。人无法仅从一个弹窗中可靠识别 ZIP 炸弹、伪装文件或重定向缺陷。审批适合处理策略无法自动决定的事项：这份特定的客户导出文件是否应因这起事故发送给这个接收方。

对于高敏感度发送、新接收方、生产环境导出文件和范围较大的诊断工件，应逐次调用审批。如果工件范围小且用途已获批准，同时目标和内容都受到约束，就可以让常规工件自动发送。让人审批每一份测试报告，只会让他们产生疲劳并习惯于不看内容就点击通过。

审批记录应绑定工件摘要、声明的用途和目标记录。任意一项发生变化，都应废弃审批。仅凭文件名不够，因为两个文件可能同名，但字节内容不同。

Sallyport 可以让代理无法接触 HTTP 上传所用的凭据，其逐密钥审批选项适合每次都需要人工决定的发送。但这并不能免除应用层工件契约的必要性，因为网关无法推断某个客户 CSV 是否应该进入支持工单。

保留两种审计视图：一种记录哪个代理运行实例获得了执行权限，另一种记录每次传输尝试及其结果。审计记录应包含摘要、实际字节数、类型判定、用途、接收方记录、决定、响应状态和时间。如果要保留机密和完整文件内容，应将其存放在其他位置，甚至可以完全不保留。

## 在代理发现拒绝路径之前测试它们

只在正常路径上成功的策略还没有完成。建立一个能记录实际收到的完整请求的隔离接收方，然后使用能触发每条边界的测试样本。

先向测试接收方发送一个普通上传请求：

```bash
curl -i -X POST https://receiver.test/attachments \\
  -H 'Authorization: Bearer test-token' \\
  -F 'file=@fixtures/diagnostic.zip;type=application/zip' \\
  -F 'case_reference=CASE-1842'
```

成功测试应确认方法、最终主机、路径、摘要和大小。真正有价值的是证明接收方什么都没有收到的测试。应断言超限请求体会在转发前被拒绝，伪造的 `text/plain` 标头无法让二进制内容通过，未列入允许列表的目标不会收到连接。

测试样本应包含以下情况：

- 扩展名有效，但签名字节不匹配。
- 压缩包解压后的大小超过限制。
- 允许的主机将请求重定向到不允许的主机。
- 包含授权标头和客户电子邮件地址的日志样本。
- 使用相同幂等令牌重试，但文件字节内容不同。

测试期间要检查服务器日志。你需要确认两件事：传输没有发生，而且自己的日志没有保留被拒绝的正文、Bearer 令牌或签名 URL。团队经常修复了网络路径，却把同样的敏感材料留在异常跟踪中。

要通过生产代理使用的同一个代理工具接口运行测试。如果面向内部的安全 HTTP 客户端允许代理包装器选择原始 URL，或绕过工件暂存路径，它就帮不上忙。

## 让安全路径比原始路径更容易使用

当获准路径缓慢、含义不清，或无法处理日常支持工作时，团队就会绕过上传控制。应构建一小组人们真正需要的工件：脱敏诊断包、范围狭窄的导出文件、生成的报告，以及边界明确的事故截图。让代理可以按名称轻松请求每一种工件。

不要把通用原始上传器加入代理工具列表。如果工程师遇到特殊情况需要它，应要求通过交互式运维路径操作，由工程师在完整上下文中选择文件和接收方。因为这种操作无法可靠地自动分类，这种不便是合理的。

成功发送和被拒绝的发送都应认真审查。大量超大诊断包意味着诊断工件设计有问题。反复尝试将日志发送到新主机，可能说明目标列表确实需要经过论证后扩展，也可能说明代理提示词正在试图绕过控制。只有把用途、文件判定和接收方决定放在一起记录，才能看出两者的区别。

我会先做一个简单的改动：从代理上传操作中移除自由填写的 URL 和任意文件路径。操作只接受暂存工件、命名用途和目标 ID 后，大小限制和内容检查才有可靠的位置发挥作用。
