文件上传的病毒扫描:应用架构选项
面向文档密集型应用的文件上传病毒扫描说明:隔离存储、扫描队列、访问控制、重试机制与安全释放工作流。

问题的通俗说明:不安全的文件进入你的应用
如果你的应用允许用户上传文档,你就在接收并处理不是自己创建的文件。在以文档为主的产品(客户门户、HR 系统、理赔应用、供应商入职)中,上传频繁,而且用户往往会分享来自邮件、共享盘或第三方的文件。这使得这些应用成为现实中的攻击目标:一次成功的恶意上传可能传播到大量下载者。
风险不仅仅是“一个病毒”。Word 或 Excel 文件可能携带恶意宏,PDF 可能被构造来利用阅读器漏洞,“发票”可能是诱导用户拨打假号码或输入凭证的钓鱼文档。有些文件更隐蔽地被下毒,比如把有效载荷藏在 ZIP 中、使用双重扩展名(report.pdf.exe),或嵌入远程内容,打开时会向外联系。
仅依赖安装在单台服务器上的简单杀毒软件并不够。上传可能触达多个应用实例、在存储系统之间移动,或从对象存储或 CDN 分发。如果任何代码路径意外暴露了原始上传文件,用户可能在扫描结束前就下载它。更新、配置错误和“临时”管理员访问也会随着时间绕过扫描。
为文件上传做病毒扫描的明确目标很简单:任何未扫描的文件,不应被未被明确授权的人下载或查看。
把“安全”定义为业务规则,而不是感觉。例如:
- 必须通过带有新签名集的恶意软件扫描
- 必须符合允许的文件类型和大小限制
- 只能从经批准的位置存储和提供
- 必须有审计记录:谁上传、何时、最终状态
- 在做出最终决定(释放或拒绝)之前必须被阻止访问
如果你使用像 AppMaster 这样的平台,把“扫描状态”作为数据模型的第一类字段,并让每个下载操作检查它。这个门槛可以防止很多昂贵的失误。
对上传文档来说,隔离(quarantine)真正的含义
“隔离”最好被视为系统中的一种状态,而不仅仅是存储中的一个文件夹。关键思想很简单:文件存在,但在应用记录了明确的扫描结果之前,任何人都不能打开或下载它。这是文件上传病毒扫描的核心。
隔离通常表现为一个带有明确状态的小生命周期。把状态显式化可以减少通过预览、直接 URL 或导出任务意外泄露不安全内容的风险。
一个实用的文件状态集如下:
- received(已接收:上传完成、尚未扫描)
- scanning(扫描中:已被工作进程拾取)
- clean(清洁:可释放)
- rejected(拒绝:发现恶意或违反策略)
- failed(失败:扫描器错误、超时或文件损坏)
隔离还需要合适的元数据以便之后执行访问控制并审计发生的事情。至少应存储:所有者(用户或组织)、状态、原始文件名和类型、校验和(用于去重和防篡改检查)、存储位置和时间戳(上传时间、扫描开始、扫描结束)。许多团队还会记录扫描器版本和扫描判定细节。
保留策略是一个决策,但应当是有意的。只在需要扫描和调试故障时保留隔离文件。较短的保留期能降低风险和成本,但仍需留出足够时间以便调查事件和支持询问“我的上传去了哪里?”的用户。
最后,决定如何处理从未完成扫描的文件。设置最大扫描时间和“过期”时间戳。到了截止时间,把文件标记为 failed,阻止访问,并有限次重试,或直接删除并要求用户重新上传。
降低风险的临时存储模式
临时存储是大多数上传问题发生的地方。文件在你的系统中,但你尚未确定它是否安全,因此需要一个既易于锁定又难以被意外暴露的存放处。
本地磁盘可以用于单一服务器,但它很脆弱。如果扩展到多个应用服务器,就必须共享存储、复制文件并保持权限一致。对象存储(例如类似 S3 的存储桶或云容器)通常对文档密集型应用更安全,因为访问规则集中管理、日志更清晰。
对文件上传病毒扫描来说,一个简单的模式是将“隔离”与“已清理”存储分离。可以用两个桶/容器来实现,这样错误更少,或者在一个桶内使用严格的前缀结构,成本更低且更易管理。
如果使用前缀,请使其不可能混淆。建议布局像 quarantine/<tenant_id>/<upload_id> 和 clean/<tenant_id>/<document_id>,不要使用用户提供的名称。不要为不同状态重用相同路径。
请记住这些规则:
- 不要允许对隔离区进行公开读取,即使是“临时的”。
- 生成服务器端对象名,而不是客户端提供的名字。
- 按租户或账户分区以减少冲击范围。
- 在数据库中存储元数据(所有者、状态、校验和),而不是把这些信息放在文件名里。
对静态数据进行加密,并严格控制谁能解密。上传 API 应能写入隔离区,扫描器应能从隔离区读取并写入已清理区,面向公众的应用只能从已清理区读取。如果云服务支持密钥策略,应把解密权限绑定到最小角色集合。
大文件需要额外注意。对于分块上传,不要在最后一个分块提交并记录预期大小和校验和之前将对象标记为“就绪”。一种常见安全做法是把分块上传到隔离区,通过扫描通过后再复制或提升对象到已清理区。
示例:在用 AppMaster 构建的客户门户中,可以将每次上传视为“pending”,存储在隔离存储桶中,仅当扫描结果把状态切换为“clean”后才显示下载按钮。
架构选项:同步扫描(inline) vs 后台扫描(background)
为文件上传添加病毒扫描时,通常在两种流程之间选择:同步扫描(用户等待)或后台扫描(异步)。正确选择更多取决于速度、可靠性以及用户上传频率,而不是“安全级别”(两者都可以安全)。
选项 1:同步扫描(用户等待)
同步扫描指上传请求在扫描器返回结果之前不会完成。它看起来简单,因为只有一步:上传、扫描、接受或拒绝。
当文件较小、上传不频繁且等待时间可预测时,通常可以接受同步扫描。例如,一个团队工具中用户每天只上传几个 PDF,可能能容忍 3 到 10 秒的暂停。缺点是扫描慢就会导致应用变慢。超时、重试和移动网络会把一次清洁文件变成糟糕的用户体验。
选项 2:后台扫描(异步)
异步扫描先存储文件,标记为“隔离”,并将任务推入扫描队列。用户会很快收到“上传已接收”的响应,但在文件被清理之前无法下载或预览。
这种方法更适合高并发、大文件和繁忙时段,因为它能把工作分散开来并保持应用响应。它还允许你把扫描工作者与主 Web 或 API 服务器分开扩展。
一个实用的混合做法是:在线上做快速检查(允许的文件类型、大小限制、基本格式校验),然后在后台做完整的杀毒扫描。这样可以在不让每个用户等待的情况下捕获明显的问题。
这里有个简单的选择指南:
- 小文件、低流量、严格的“必须立即知道”流程:同步扫描
- 大文件、频繁上传或扫描时间不可预测:后台扫描
- 对上传响应时间有严格 SLA:后台扫描并提供明确的状态 UI
- 混合工作负载:混合模式(先快速检查,全面扫描异步进行)
如果你用 AppMaster 构建,这个选择通常映射为同步 API 端点(同步扫描)或将扫描工作入队并在结果返回时更新文件状态的 Business Process(异步)。
逐步指南:构建异步扫描队列
异步扫描意味着你接受上传、把它锁定在隔离区,并在后台扫描。只有扫描器宣告安全后,用户才有访问权。这通常是文档密集型应用最实用的恶意软件扫描架构。
1)定义队列消息(保持精简)
把队列当成待办列表。每次上传创建一条指向文件的消息,而不是把文件本身放进队列。
一个简单的消息通常包含:
- 文件 ID(或对象键)和租户或项目 ID
- 上传者用户 ID
- 上传时间戳和可选的校验和(有助于校验)
- 尝试次数(或单独的重试计数器)
避免把原始字节放入队列。大负载会突破限制、增加成本并扩大暴露面。
2)构建工作者流程(获取、扫描、记录)
工作者拉取消息,从隔离存储获取文件,扫描它,然后写回决策。
清晰的流程示例:
- 根据 ID 从隔离存储获取文件(私有桶或私有卷)
- 运行扫描器(AV 引擎或扫描服务)
- 把结果写回数据库:状态(clean、infected、error)、扫描器名/版本和时间戳
- 若为 clean:把文件移动到批准的存储或切换访问标志以使其可下载
- 若为 infected:保持隔离(或删除)并通知相关人员
3)做到幂等(可安全重处理)
工作者会崩溃、消息会被重复投递、重试会发生。设计时要保证对同一文件的重复扫描不造成问题。使用单一事实源记录,例如 files.status,并只允许有效的状态转换,例如:uploaded -> scanning -> clean/infected/error。如果工作者看到状态已是 clean,它应该停止并应答消息。
4)控制并发(避免扫描风暴)
为每个工作者和每个租户设置并发限制。限制同时运行的扫描数量,并考虑为大文件使用单独队列。这样可以防止某个高流量客户耗尽所有扫描容量。
5)用重试和审计处理失败
对临时错误(扫描器超时、网络问题)使用重试,并设置小的最大尝试次数。超过后把消息发到死信队列以供人工审查。
保留审计记录:谁上传了文档、何时进入隔离、哪个扫描器运行、它做出的决定,以及谁批准或删除了文件。该日志对于客户门户和合规性同样重要。
访问控制:保持隔离文件真正私有
隔离不仅是数据库中的一个状态。它承诺在文件被证明安全之前没有人能打开它。最安全的规则很简单:切勿通过公共 URL 提供隔离文件,即使是“临时”的也不行。
良好的下载流程是枯燥但严格的。应用应把每次下载当作受保护的操作,而不是像取图片那样简单。
一个好的下载流程:
- 请求下载
- 检查该用户对该文件的权限
- 检查文件状态(隔离、clean、rejected)
- 仅当状态为 clean 时交付文件
如果使用签名 URL,保持相同的理念:仅在权限和状态检查通过后生成,且使其短期有效。短过期能减少链接通过日志、截图或转发邮件泄露时造成的损害。
基于角色的访问可以帮助你避免演变成漏洞的“特殊情况”逻辑。文档密集型应用的典型角色有:
- 上传者:可查看自己的上传及其扫描状态
- 审核者:可查看清洁文件,有时可在安全审查工具中查看隔离文件
- 管理员:可调查、重新扫描并在必要时覆盖访问
- 外部用户:仅可访问明确与其共享的文档
还要防止 ID 猜测。不要暴露像 12345 这样的自增文件 ID。使用不透明 ID,并始终按用户与文件分别授权(而不仅仅是“任何登录用户”)。即便存储桶是私有的,一个粗心的 API 端点仍可能泄露隔离内容。
在为文件上传做病毒扫描时,访问层是大多数实际故障发生的地方。在像 AppMaster 这样的平 台上,你应在 API 端点和业务逻辑中强制这些检查,从而使隔离默认保持私有。
释放、拒绝和重试:处理扫描结果
文件一旦完成扫描,最重要的是把它转入一个明确定义的状态,并让下一步可预测。如果你在为文件上传做病毒扫描,把扫描结果视为一道闸门:在闸门允许之前,任何东西都不会变为可下载。
一个简单的结果集合覆盖了大多数实际系统:
- Clean:从隔离中释放文件并允许正常访问。
- Infected:永久阻止访问并触发受感染文件的工作流程。
- Unsupported:扫描器无法评估此类型(或受密码保护)。保持阻止。
- Scan error:临时失败(超时、服务不可用)。保持阻止。
对用户的提示应明确且平和。避免像“你的账户被入侵”这样的惊悚措辞。更好的做法是:“文件正在检查中。你可以继续操作。”如果文件被阻止,告诉用户下一步可以做什么:“上传其他文件类型”或“稍后再试”。对于不支持的文件,要具体说明(例如“加密压缩包无法被扫描”)。
对于被感染的文件,尽早决定是删除还是保留。删除更简单且降低风险。保留有助于审计,但前提是你把它存放在隔离的区域、严格控制访问并设置短保留期,同时记录谁能看到它(最好除了安全管理员外无人可见)。
重试有用,但只针对可能是暂时性的错误。设置小规模的重试策略,以免建立无止境的积压:
- 对超时和扫描器停机进行重试。
- 不要对“infected”或“unsupported”重试。
- 限制重试次数(例如 3 次),然后标记为 failed。
- 在尝试间加入退避以避免过载。
最后,把重复失败视为运维问题,而不是用户的问题。如果大量文件在短时间内出现“scan error”,要告警并暂停新释放。在 AppMaster 中,你可以在数据库中建模这些状态,并通过内置的消息模块把通知路由给相关人员,让他们及时获知故障。
示例场景:大量文档的客户门户
客户门户允许客户为每个项目上传发票和合同。这是文件上传病毒扫描非常重要的常见场景,因为用户会直接拖入桌面上随手保存的东西,包括来自他人的转发文件。
当客户上传 PDF 时,门户把它保存到临时私有位置,并在数据库中新建一条状态为 Pending scan 的记录。该文件尚不可下载。一个扫描工作者从队列中拉取文件,运行扫描,然后把记录更新为 Clean 或 Blocked。
在界面上,客户会立即看到文档出现,但带有明显的 Pending 徽章。文件名和大小可见以确认上传成功,但下载按钮在扫描变为 clean 之前是禁用的。如果扫描比预期慢,门户可以显示像“我们正在检查此文件的安全性,请稍后重试”的简单提示。
如果扫描器标记了文档,客户会看到 Blocked 以及简短非技术性的说明:“此文件未通过安全检查”。支持和管理员有单独视图,包含扫描原因和下一步操作。他们可以:
- 保持阻止并请求重新上传
- 删除并记录原因
- 如果策略允许,可把某些情况标记为误报
在争议情况下(“我昨天上传了,你却丢失了”),良好的日志很关键。保存上传接收时间、扫描开始、扫描结束、状态变更和操作者的时间戳。还要存储文件哈希、原始文件名、上传者账户、IP 地址和扫描器返回的结果代码。如果在 AppMaster 上构建,Data Designer 加上简单的 Business Process 流可以管理这些状态和审计字段,同时不向普通用户暴露隔离文件。
常见错误导致的实际安全漏洞
大多数上传安全故障不是花哨的攻击,而是小的设计选择让不安全的文件表现得像普通文档。
一个典型问题是竞态条件:应用接受上传并返回一个“下载”URL,用户(或其它服务)在扫描完成前就能获取文件。如果你要为文件上传做病毒扫描,把“已上传”和“可用”视为两个不同的状态。
常见错误包括:
- 在同一桶/文件夹混合存放已清理和隔离文件,然后依赖命名规则。一次错误的权限或路径猜测就会使隔离变得毫无意义。
- 信任文件扩展名、MIME 类型或客户端检查。攻击者可以把任何东西改名为 .pdf 而你的 UI 无法识别。
- 未对扫描器停机做出规划。如果扫描器慢或离线,文件会长期处于 pending,团队可能开始添加不安全的手动覆盖。
- 让后台工作者跳过主 API 的授权规则。能读取“任何文件”的工作者是一种静默的权限升级。
- 发布易猜的隔离项 ID(如自增数字),即便你认为内容受保护也会出问题。
测试也是一个薄弱点。团队用少量小型干净文件测试就自认为完成。你还需要测试大文件、损坏文件和受密码保护的文档,因为这些正是扫描器和解析器失败或超时的地方。
一个简单的真实例子:客户门户用户上传了名为 “contract.pdf” 的文件,实际上它是一个在归档内被改名的可执行文件。如果门户立刻把它提供下载,或支持团队能在没有适当检查的情况下访问隔离,那么你就为其他用户创建了直接传递路径。
上线前的快速清单
在上线文件上传病毒扫描之前,对那些团队通常会默认“没问题”但日后会发现不对的地方做最后检查。目标很简单:不安全的文件不应因为有人猜到 URL、重试请求或使用旧缓存链接而变得可读。
从用户流开始。任何下载、预览或“打开文件”的操作都应在请求时重新检查文件的当前扫描状态,而不是仅在上传时检查。这可以保护你免受竞态条件(用户立即点击)、扫描结果延迟以及文件被重新扫描等边缘情况的影响。
把以下预上线清单作为最小基线:
- 隔离存储默认私有:无公开桶访问、无“持有链接即可访问”、无从原始对象存储直接服务。
- 每个文件记录都有所有者(用户、团队或租户)和清晰的生命周期状态,如 pending、clean、infected 或 failed。
- 扫描队列和工作者有有界重试、明确的退避规则,并在项目卡住或反复失败时产生告警。
- 存在上传、扫描结果和下载尝试(包括被阻止的尝试)的审计日志,记录谁、何时、为什么。
- 存在人工覆盖机制用于极少数情况,但仅限管理员、可被记录且有时限(没有默默的“标记为 clean”按钮)。
最后,确保你可以端到端观察系统。你应该能回答:“现在有多少文件正在等待扫描?”和“哪些租户出现了失败?”如果在 AppMaster 上构建,请在 Data Designer 中建模文件生命周期,并在 Business Process Editor 中强制状态检查,使规则在 Web 和移动端保持一致。
下一步:把设计变成交付应用
先把文件可能处于的确切状态写下来,以及每个状态允许什么。保持简单明确:"uploaded"、"queued"、"scanning"、"clean"、"infected"、"scan_failed"。然后在每个状态旁边加入访问规则。谁能看到文件、下载它或在其仍不受信任时删除它?
接着,选择与你的流量和用户体验目标匹配的方法。同步扫描更易解释,但会让上传感觉缓慢。异步扫描更适合文档密集型应用,但会增加状态、队列和“待处理”UI。
把设计落地的一种实用方法是使用真实文档(PDF、Office 文件、图像、归档)和真实用户行为(并发上传、取消、重试)对整个流程做端到端原型。不要仅停留在“扫描器有效”这一层面。验证应用在任何情况下都不会意外提供隔离文件。
这是一个一周内可执行的简单构建计划:
- 在一页内定义文件状态、转换和访问规则
- 选择同步、异步或混合的文件上传病毒扫描方法并记录权衡
- 实现上传 -> 隔离存储 -> 扫描任务 -> 结果回调,并带上审计日志
- 构建用户会看到的 UI 状态(pending、blocked、failed、approved)
- 从第一天起添加监控:积压大小、失败率和到达 clean 的时间
如果你无代码构建,AppMaster 可以帮助你建模文件元数据(状态、所有者、校验和、时间戳)、构建上传和审查界面,并用业务逻辑和类队列处理编排扫描工作流。这样你能尽早测试真实产品流,然后加固关键部分:权限、存储分离和可靠重试。
最后,定义“良好”的量化标准。在发布前设置告警阈值,以便在用户注意到之前发现卡住的扫描和上升的失败率。


