把文件上传规则写成可观察的结果

使用 AI 辅助编写与复核。

本文目录

进度条到了 100%,但新附件还需要检查。界面应该显示“Uploaded(已上传)”“Ready(可用)”,还是“Could not check(无法检查)”?让 AI 编程 Agent 开始制作上传表单前,先回答这个问题。

一份有用的上传 brief 应定义哪些文件符合条件、何时变得可用,以及应用无法作出判断时怎么办。从几份虚构文件描述出发,让它们分别经过同一套规则。

OWASP 将扩展名、声明的内容类型、内容检查、大小限制、存储和访问权限视为不同问题。客户端提交的内容类型可能不真实,文件签名单独也不能证明安全。其指南支持分层检查,但不提供下文的具体产品策略。OWASP 文件上传速查表

选定一种附件任务

假设一个虚构笔记本允许为记录 N17 添加 PNG 和 JPEG 参考图。Mira 可以在该记录上添加和获取图片,Sol 无权访问它。这些都是合成权限标签;示例没有使用任何账户或文件。

为这个笔记本选择以下规则:

  • 文件必须为 1 到 2,097,152 字节,包含上下限。限制针对文件字节,不包含请求封装的额外开销。界面显示“Maximum 2 MiB per image”。
  • 最后一个扩展名前的基本名称由 1–60 个 ASCII 字母、数字、空格、下划线或连字符组成,以字母或数字开头,也以字母或数字结尾。只有一个字符的基本名称同时满足首尾条件。最后一个扩展名必须恰好是小写 .png、.jpg 或 .jpeg。
  • .png 要求声明为 image/png;.jpg 和 .jpeg 要求声明为 image/jpeg。不匹配时拒绝,而不是默默纠正。
  • 服务对内容的分类必须与声明类型一致。另一个已配置的内容检查也必须在开放文件前返回通过。检查器不可用或结果未知,绝不能当成通过。
  • 上传名称只是用于显示的元数据。应用分配独立的存储标识符,不把用户提交的名称作为存储路径。
  • 接收与检查在私有暂存区进行。获取文件必须同时满足 READY 状态和对 N17 的当前访问权。

这些严格的文件名和一致性规则,是本示例的选择。其他应用可以规范化扩展名或接受别的格式,但 brief 应明确说明。本策略不包含压缩包、文档转换或可执行附件。

为每项决定指定边界

问题 界面职责 服务职责 服务无法确认时
这个身份能否向 N17 添加附件? 隐藏不可用控件;解释已知的拒绝 创建附件前检查当前权限 拒绝;不在记录上创建附件
文件是否在字节限制内? 显示限制,并提前识别明显超限 接收文件字节时执行限制 停止接受该附件,不开放访问
提交的名称是否符合策略? 显示有用的命名错误 验证名称,并分配独立存储标识符 拒绝附件
扩展名是否与声明类型一致? 提供提前修正的机会 再次应用声明类型映射 拒绝附件
内容分类是否一致? 显示返回的状态 独立于声明对内容分类 保持不可用,并显示检查错误
配置的内容检查是否通过? 将检查状态与传输进度分开显示 要求检查完成且通过 保持不可用;准确显示失败或不确定
这个身份现在能否获取图片? 只有在已授权时显示可用附件 获取文件时检查当前记录访问权和开放状态 拒绝获取,不返回文件内容

界面提供快速反馈,服务作出接收和访问决定。即使请求绕过了隐藏按钮或本地检查而到达,这些决定仍然适用。

逐一推演九份虚构描述符

以下内容类别与检查结果,是纸面练习预先给定的输入,不是检查图片、运行扫描器或上传字节所得的结果。除 F5 外,每行的上传者都是 Mira。所有文件名都符合命名规则。

ID 提交的名称 字节数 声明类型 给定的内容类别 单独内容检查的结果 预期结果
F1 sketch.png 2,097,152 image/png PNG Pass 恰好达到限制,符合接收条件;私有接收、检查,然后进入 READY
F2 sketch-large.png 2,097,153 image/png PNG Not run REJECTED:FILE_TOO_LARGE
F3 banner.png 2,048 image/jpeg Not examined Not run REJECTED:NAME_TYPE_MISMATCH
F4 preview.jpg 4,096 image/jpeg PNG Not run REJECTED:CONTENT_TYPE_MISMATCH
F5 private.png 1,024 image/png Not examined Not run 上传者为 Sol:REJECTED、NOT_ALLOWED;不创建附件
F6 draft.png 1,024 image/png Unknown Not run CHECK_FAILED:TYPE_UNCONFIRMED;不可用
F7 diagram.png 1,400 image/png PNG Fail REJECTED:CONTENT_CHECK_FAILED;不可用
F8 chart.png 1,500 image/png PNG Timeout CHECK_FAILED:CHECK_UNAVAILABLE;不可用
F9 layout.jpeg 1,750 image/jpeg JPEG Pending CHECKING;得到明确结果前不可用

F1 精确回答了边界问题:本策略中的“最多”包含上限。F2 比上限多一个字节。F3 在开始检查内容前,元数据就不一致。F4 的元数据一致,但与内容分类相矛盾。这些应对应不同错误信息和不同检查。

空文件同样被拒绝,错误为 EMPTY_FILE。diagram.gif 这样的名称会得到 NAME_NOT_ALLOWED,即使其内容声明为 PNG 也不例外。零字节文件或无效名称,不会因为应用希望后续步骤能修好它,就进入“检查中”。

本示例按以下顺序评估:权限、名称、字节范围、扩展名与声明的一致性、内容类别,再到独立的内容检查。真实传输期间,大小关卡还必须在接收过程中阻止超量数据。如果多项条件有误,第一项适用的决定给出可见错误。后续阶段不能把已经拒绝的文件变成已接受附件。

为每种状态写清用户能看到的含义

状态 在本示例中的含义 用户看到什么 文件获取
LOCAL_SELECTED 选择仅存在于客户端 已选择,尚未发送 没有远端附件
RECEIVED_PRIVATE 符合接收条件的字节已进入私有暂存区 已接收,等待检查 不可用
CHECKING 某项必要检查仍待完成 正在检查图片 不可用
READY 必要检查通过,且开放状态已记录 可作为附件使用 只有具备当前记录访问权才允许
REJECTED 某项策略条件未满足 具体拒绝原因,以及可能的修正方式 不可用
CHECK_FAILED 服务未能确认某项必要结果 无法完成检查,图片不可用 不可用
EXPIRED 不可用暂存记录达到过期时限 暂存图片已过期,请重新选择 不可用,包括仍待移除字节期间

只有服务能确认 READY。传输进度达到 100%,不会触发这一转换。包含内部标识符的附件回执也不会。

假设 F9 后来收到完成且通过的检查结果,可以从 CHECKING 转为 READY。如果收到失败结果,则转为 REJECTED。检查器超时则转为 CHECK_FAILED。重试检查必须是用户针对同一暂存文件作出的明确操作,而且只有暂存记录仍存在、用户也仍有权限时才允许。它回到 CHECKING,而不是直接进入 READY。

这个笔记本选择让不可用的暂存对象在接收 24 小时后过期。重新检查不会延长时限。过期时,把暂存记录标为已过期并请求移除字节;只有存储操作得到确认,才能报告删除完成。存储机制和清理实现仍待选择。过期策略不是字节已实际移除的证据。

F1 进入 READY 后,Mira 可以获取它,Sol 仍然不可以。分享其标识符不会改变这个决定。如果 Mira 对记录的访问权被移除,她下一次获取也会被拒绝。真实设计必须让规则覆盖实际交付路径,包括生成的下载链接,而不只是附件列表中的一句承诺。

复制一份结果明确的 brief

为虚构记录 N17 设计图片附件流程。
只使用本文的合成描述符 F1-F9。不要创建图片文件、
检查私人文件、调用扫描服务或上传数据。

严格应用规定的 PNG/JPEG、文件名和包含上下限的字节规则。
Mira 可以添加和获取附件;Sol 没有记录访问权。
即使跳过本地检查,也必须由服务决定权限并执行各项关卡。
绝不能把提交的名称作为存储路径。

把私有接收、检查、就绪、拒绝和无结论的检查分开。
只有必要检查完成且通过,才允许 READY。
为每份描述符采用准确的结果和错误码,另外覆盖空文件和
不支持名称的情况。传输进度不等于开放文件访问。

展示暂存文件重试、24 小时过期及已确认清理的状态。
要求实际获取路径(包括链接)检查当前记录权限;
标识符或旧 UI 状态不能授予访问权。

返回职责矩阵、状态转换、用户文案和预期结果。
列出尚未选择的存储与内容检查机制,供实现审阅。
不要把设计描述成安全上传实现或已完成的集成。

当每份描述符都能导向一个可理解的状态和明确的下一步时,这份 brief 就可以用于设计讨论。实现阶段再解决服务如何执行这些决定,让用户不必猜“上传成功”究竟意味着什么。