跳转到帖子

背景

文件上传是 Web 系统里很常见的功能,头像、附件、工单截图、导入文件都离不开它。也正因为入口常见、实现差异大,它一直是代码审计和安全测试中的高频风险点。

不少团队在处理上传功能时,容易只做一层校验,比如检查扩展名、限制 Content-Type,或者前端限制文件类型。单独看这些措施都有意义,但如果缺少整体防护链路,仍然可能出现上传可执行脚本、路径穿越、存储型 XSS、解析差异、资源耗尽等问题。

这篇文章结合平时审计中遇到的典型场景,整理一套更稳妥的文件上传审计与防护思路。重点放在风险识别和安全加固,不涉及未授权攻击或恶意利用。

常见问题场景

在实际项目中,文件上传相关问题通常不是单点失误,而是多个细节叠加导致的。比较常见的场景包括:

  • 只在前端限制文件类型,后端没有重新校验。
  • 后端只判断扩展名,没有校验文件真实内容。
  • 上传目录位于 Web 可访问路径下,且服务器可能解析脚本文件。
  • 文件名直接使用用户传入值,存在覆盖、路径穿越或特殊字符问题。
  • 允许上传 SVG、HTML、PDF 等可携带脚本或主动内容的文件。
  • 缺少文件大小、数量、频率限制,可能导致存储或内存压力。
  • 错误信息过于详细,暴露服务器路径、组件版本或校验逻辑。

其中最危险的一类,是上传文件被服务器当作可执行内容处理。即使业务只允许图片,如果上传目录配置不当,也可能因为解析规则、历史兼容配置或中间件差异引入风险。

技术分析

审计文件上传功能时,可以围绕“入口校验、文件处理、存储位置、访问方式、运行权限”五个层面展开。

1. 入口校验不能只依赖客户端

前端 accept 属性、JavaScript 校验、移动端文件选择限制,只能改善用户体验,不能作为安全边界。真正的校验必须在服务端完成。

常见错误示例是后端直接信任请求中的 Content-Type:

Content-Type: image/png

这个字段由客户端提交,可以被任意构造。服务端如果只基于它判断文件类型,安全性不足。更可靠的方式是同时结合扩展名白名单、文件头特征、解析库验证结果和业务允许范围。

2. 扩展名校验要使用白名单

文件类型建议采用白名单,而不是黑名单。黑名单很容易遗漏大小写、复合后缀、历史解析规则或不常见扩展名。

推荐思路:

  • 只允许业务确实需要的类型,例如 jpg、jpeg、png、gif、pdf。
  • 统一转换扩展名为小写后再判断。
  • 不要允许用户自定义最终存储扩展名。
  • 避免使用“包含判断”,应使用精确匹配。

例如伪代码层面可以这样处理:

allowed_ext = {"jpg", "jpeg", "png", "gif", "pdf"}
ext = get_extension(filename).lower()
if ext not in allowed_ext:
    reject("unsupported file type")

需要注意的是,扩展名白名单只是第一步,不能单独作为最终安全判断。

3. 校验文件真实内容

对于图片类文件,可以使用成熟图片处理库重新解析,而不是仅判断文件头几个字节。能被图片库正常解码并重新编码的文件,风险会低很多。

常见做法是上传后生成新的图片文件,丢弃原始文件中的附加数据和异常结构。例如头像上传可以统一转成 JPEG 或 PNG,并重新生成文件内容。

# 示例:使用 ImageMagick 做图片重新编码的思路
magick input.tmp -strip -resize 512x512 output.jpg

这里的关键点不是具体工具,而是“不要原样信任用户上传的文件”。重新编码、去除元数据、限制尺寸,可以同时降低脚本夹带、隐私元数据泄露和资源消耗风险。

如果业务允许 PDF、Office 文档等复杂格式,应额外评估主动内容、宏、嵌入脚本、外部链接和预览组件漏洞风险。对于高风险文件类型,建议隔离存储和异步安全扫描。

4. 文件名必须重写

用户上传的原始文件名不适合作为最终存储文件名。它可能包含特殊字符、路径分隔符、超长字符串,甚至引发日志污染或展示层问题。

建议服务端生成随机文件名或基于内容哈希生成文件名,并将原始文件名仅作为普通文本字段保存,展示时进行 HTML 转义。

safe_name = uuid4() + "." + safe_ext
store_path = upload_root + "/" + date_path + "/" + safe_name

同时要确保拼接路径后仍位于预期目录下,避免路径穿越:

real_path = realpath(store_path)
if not real_path.startswith(realpath(upload_root)):
    reject("invalid path")

5. 上传目录不应具备执行能力

这是文件上传防护里非常关键的一点。即使前面校验存在疏漏,只要上传目录不会执行脚本,风险也会显著降低。

对于 Nginx,可以将上传目录配置为仅静态访问,并避免落入 PHP、JSP、ASP.NET 等脚本解析规则。示例思路如下:

location /uploads/ {
    alias /data/app/uploads/;
    autoindex off;
    add_header X-Content-Type-Options nosniff;
}

location ~* ^/uploads/.*\.(php|php5|phtml|jsp|jspx|asp|aspx)$ {
    return 403;
}

如果条件允许,更推荐把上传文件放到独立对象存储或独立静态域名下,与主站应用域隔离,避免同源风险。对于用户可控内容,单独域名可以降低 Cookie、LocalStorage 和同源接口被影响的概率。

6. 返回头与访问控制

上传文件的访问方式也需要审计。常见建议包括:

  • 对不需要浏览器直接渲染的文件,使用下载方式返回。
  • 设置正确的 Content-Type,不要让浏览器自行猜测。
  • 增加 X-Content-Type-Options: nosniff。
  • 敏感附件应做鉴权,不要只依赖不可猜测 URL。
  • 禁止在同源环境下直接预览高风险用户文件。

对于附件下载接口,可以参考如下响应头思路:

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="download.bin"
X-Content-Type-Options: nosniff

关键审计步骤

实际代码审计时,我一般会按下面顺序检查,效率相对高一些。

第一步:定位所有上传入口

不要只看“上传头像”这种显眼功能。后台导入、富文本编辑器、客服附件、日志上传、插件安装、主题上传、API 批量导入,都可能包含上传能力。

可以通过路由、控制器、关键词快速定位:

upload
multipart
MultipartFile
move_uploaded_file
FormFile
saveAs
writeFile
transferTo
InputStream

第二步:确认服务端校验逻辑

重点看以下问题:

  • 是否只依赖前端或网关校验。
  • 是否使用扩展名白名单。
  • 是否校验真实文件内容。
  • 是否限制文件大小、图片尺寸、压缩包解压数量。
  • 是否允许 SVG、HTML、XML 等高风险格式。
  • 错误处理是否暴露本地路径和堆栈。

第三步:检查存储路径和访问路径

要分清“保存到哪里”和“用户如何访问”。有些系统保存路径在应用目录下,有些通过静态映射暴露出来。需要确认上传目录是否可能被脚本引擎解析。

重点关注:

  • 上传目录是否在 WebRoot 下。
  • Nginx、Apache、Tomcat、IIS 是否存在特殊解析规则。
  • 是否存在软链接、目录穿越或路径拼接问题。
  • 是否允许覆盖已有文件。
  • 是否按租户、用户、业务做隔离。

第四步:检查后续处理链路

文件上传后通常还会进入缩略图、OCR、文档预览、解压、转码、病毒扫描等处理流程。这些异步任务同样可能引入风险。

例如压缩包处理需要防范 Zip Slip、压缩炸弹和超深目录;图片处理需要防范超大尺寸图片导致内存占用异常;文档预览服务应尽量容器化隔离,避免处理复杂格式时影响主业务。

第五步:验证权限模型

很多上传问题并非文件类型本身,而是权限控制缺失。比如用户 A 上传的合同附件,用户 B 通过枚举 ID 或 URL 能访问,这属于典型越权。

检查点包括:

  • 上传接口是否校验登录态和业务权限。
  • 下载接口是否按资源归属鉴权。
  • 临时文件是否会被未授权访问。
  • 对象存储链接是否长期有效且可被传播。

防护建议清单

防护点建议做法
类型控制服务端白名单校验扩展名,并结合内容解析验证
文件名服务端重命名,原始文件名仅作为文本保存
存储位置上传目录禁止脚本执行,优先使用独立静态域名或对象存储
内容处理图片重新编码,文档和压缩包进入隔离处理流程
访问控制敏感附件下载必须鉴权,避免仅依赖不可猜测链接
响应头设置 Content-Disposition、Content-Type、nosniff
资源限制限制大小、数量、频率、图片尺寸、解压后总量
日志审计记录上传人、时间、文件哈希、业务 ID、校验结果

容易忽略的细节

  • SVG 虽然常被当作图片,但它是 XML 格式,可能包含脚本、外链和事件属性,默认应谨慎处理。
  • 富文本编辑器上传功能经常引入第三方组件,升级或配置不当时风险较高。
  • 对象存储桶策略需要单独审计,避免误设为公共写入或公共列目录。
  • 图片 EXIF 可能包含定位、设备信息,涉及隐私场景建议清理。
  • 不要在日志中直接输出未转义的原始文件名,避免污染日志展示页面。
  • 文件哈希可以辅助去重和溯源,但不能替代安全扫描。

事件响应中的排查思路

如果怀疑系统曾出现上传相关风险,可以先从以下几个方向排查:

  • 统计上传目录内异常扩展名、双后缀文件、超大文件和近期新增文件。
  • 检查 Web 访问日志中上传目录下的异常访问路径和非常规状态码。
  • 比对上传文件哈希、上传用户、业务单据和访问来源。
  • 检查服务器配置是否存在上传目录脚本解析规则。
  • 确认是否有异常计划任务、异常进程或未知外联行为。

排查时建议先保全日志和文件样本,再进行清理和加固。直接删除文件可能影响后续定位根因。

总结

文件上传安全不能只靠某一个校验点。比较稳妥的做法是:入口白名单校验、内容真实解析、服务端重命名、上传目录禁止执行、访问权限控制、响应头约束、后续处理隔离。

从代码审计角度看,最值得优先确认的是三件事:服务端是否真正校验、上传目录是否可执行、下载访问是否鉴权。只要这三处没有明显短板,大多数常见上传风险都能被压住。

长期维护上,建议把上传能力封装成统一组件,不要每个业务模块各写一套。文件上传看似简单,但一旦散落在项目各处,后续审计和加固成本会明显增加。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…