跳转到帖子

背景

文件上传是 Web 安全测试里很常见的一类入口,但真正能造成风险的点通常不是“能不能上传文件”这么简单,而是上传后的文件存储位置、访问方式、扩展名处理、内容校验、解析规则、权限控制等多个环节共同失效。

这篇整理基于一次内部测试中遇到的文件上传缺陷,重点放在漏洞分析、复现思路和修复验证。文中不提供可直接用于攻击的恶意载荷,也不讨论未授权入侵场景,适合用于代码审计、自查和安全加固。

问题场景

某业务系统提供头像、附件、工单图片等上传能力。表面看接口限制了扩展名,只允许上传图片类文件。但测试中发现,服务端对文件类型的判断主要依赖客户端传入的文件名和 Content-Type,且上传目录位于 Web 可访问路径下。

简化后的风险链路如下:

  • 前端限制扩展名,但后端校验不足。
  • 服务端将用户可控文件名参与拼接保存路径。
  • 上传目录可被直接访问。
  • 部分中间件或框架存在错误解析配置。
  • 文件上传后缺少二次处理和隔离。

这类问题在老系统、快速迭代项目、后台管理系统里比较常见。单点问题不一定立刻形成高危,但多个弱点叠加后风险会明显放大。

技术分析

文件上传安全通常要关注三个层面:输入校验、存储隔离、访问控制。

1. 输入校验不应信任客户端字段

很多系统会读取上传请求中的 filename 和 Content-Type 来判断文件类型,例如认为 Content-Type 为 image/png 就是图片。这个判断是不可靠的,因为这些字段都来自客户端,请求方可以自行构造。

常见但不可靠的写法包括:

if file.content_type in ['image/png', 'image/jpeg']:
    save(file)

更稳妥的做法是结合文件头、解码结果、扩展名白名单和业务场景做多重判断。比如图片上传可以尝试用图像库重新解码,并将其重新编码后保存,避免原始内容直接落盘。

2. 文件名和路径需要完全去用户化

上传文件名不建议直接使用用户传入值。原因包括路径穿越、特殊字符、同名覆盖、日志污染、跨平台兼容问题等。

推荐做法是服务端生成随机文件名或基于内容摘要生成文件名,并固定扩展名来源。示例逻辑如下:

import uuid
from pathlib import Path

ALLOWED_EXT = {'jpg', 'jpeg', 'png', 'gif'}

origin_name = uploaded_file.filename
ext = origin_name.rsplit('.', 1)[-1].lower()
if ext not in ALLOWED_EXT:
    raise ValueError('invalid extension')

safe_name = f"{uuid.uuid4().hex}.{ext}"
save_path = Path('/data/upload/images') / safe_name

这里的关键点不是 UUID 本身,而是不要让用户输入直接影响最终路径和文件名。

3. 上传目录不应具备脚本执行能力

即使业务只允许图片上传,也应该假设校验可能被绕过,因此上传目录必须做执行隔离。比较推荐的方式是将上传文件存储在 Web 根目录之外,通过后端接口、对象存储或静态资源服务进行受控访问。

如果确实需要通过 Nginx 提供静态访问,应确保上传目录不会被任何脚本引擎解释执行。例如:

location /uploads/ {
    alias /data/upload/;
    autoindex off;
    default_type application/octet-stream;

    location ~* \.(php|jsp|jspx|asp|aspx|cgi|pl|py|sh)$ {
        return 403;
    }
}

对于 PHP 环境,还需要特别注意 PHP-FPM 的解析配置,避免错误地将非预期文件交给解释器处理。

关键复现步骤

以下步骤适用于授权测试或内部自查,重点是判断风险面,不涉及恶意控制操作。

1. 确认上传接口和参数

先通过浏览器开发者工具或代理工具观察上传请求,记录接口路径、表单字段、鉴权方式、返回结果。重点关注:

  • 上传字段名,例如 file、image、attachment。
  • 是否携带业务参数,例如 type、path、module。
  • 返回值是否包含文件访问 URL。
  • 失败时服务端返回的错误信息是否过于详细。

2. 验证扩展名白名单

上传正常图片,确认业务流程。然后用非允许扩展名测试服务端是否拒绝。这里不需要构造恶意文件,只要使用普通文本文件即可。

curl -i -X POST  \
  -H "Authorization: Bearer <token>" \
  -F "[email protected];type=text/plain"

如果服务端只在前端限制,直接请求接口时仍然返回成功,就说明后端校验存在缺口。

3. 验证 Content-Type 信任问题

可以观察服务端是否仅依赖 Content-Type。比如使用普通文件但声明为图片类型,查看是否被接受。

curl -i -X POST  \
  -H "Authorization: Bearer <token>" \
  -F "[email protected];type=image/png"

如果此类请求被接受,需要进一步检查服务端是否做了真实文件内容识别。

4. 检查文件访问方式

若返回 URL,访问该 URL 查看响应头和行为。建议关注:

  • Content-Type 是否合理。
  • Content-Disposition 是否强制下载。
  • 是否允许目录索引。
  • 是否存在跨用户访问问题。
  • 是否可枚举文件名。

例如:

curl -I  检查权限边界

附件类上传尤其要注意访问控制。常见问题是上传接口需要登录,但文件访问 URL 不需要登录,导致任何拿到链接的人都能访问。

如果附件涉及工单、合同、内部文档等,应通过后端鉴权后再返回文件内容,而不是直接暴露静态路径。

代码审计关注点

在代码层面,可以重点搜索以下逻辑:

  • 是否直接使用 request 中的 filename。
  • 是否使用字符串拼接生成保存路径。
  • 是否只判断 Content-Type。
  • 是否仅通过扩展名黑名单拦截。
  • 是否存在可控目录参数,如 path、folder、module。
  • 上传成功后是否直接返回真实存储路径。
  • 是否有异步处理、缩略图生成、解压等二次处理逻辑。

黑名单方式通常容易漏掉边界情况。更可靠的是白名单加内容校验,并且不同业务类型使用不同上传策略。

修复建议

建议按优先级处理:

风险点建议措施
文件类型伪造扩展名白名单、文件头识别、图片重新解码保存
路径可控服务端生成文件名,禁止用户输入参与路径拼接
脚本执行风险上传目录放到 Web 根目录外,禁用脚本解析
越权访问文件下载接口做鉴权,避免静态路径直出敏感附件
文件覆盖随机文件名或内容摘要命名,避免同名覆盖
资源消耗限制文件大小、尺寸、数量和上传频率

如果是图片上传,比较稳的处理流程如下:

  • 限制请求体大小。
  • 校验扩展名白名单。
  • 读取文件头并识别真实类型。
  • 使用图像库解码。
  • 重新编码为统一格式。
  • 保存到隔离目录。
  • 返回受控访问地址。

修复验证

修复完成后不要只验证“正常图片能上传”,还应覆盖异常场景:

  • 非白名单扩展名是否被拒绝。
  • 伪造 Content-Type 是否无效。
  • 超大文件是否被拒绝。
  • 畸形图片是否被拒绝或安全处理。
  • 上传路径是否不可控。
  • 返回 URL 是否不可预测。
  • 其他用户是否无法访问无权限附件。
  • 上传目录是否无法执行脚本类文件。

可以将这些检查加入回归测试,避免后续业务迭代时重新引入问题。

注意点

  • 不要把前端校验当作安全措施,前端限制只能提升用户体验。
  • 不要依赖扩展名黑名单,遗漏情况很多。
  • 不要把上传文件放在可执行目录下。
  • 不要直接返回服务器真实路径。
  • 不要忽略缩略图、压缩包解压、文档预览等二次处理链路。
  • 对象存储也需要配置访问权限,公开桶同样可能造成数据泄露。
文件上传问题的核心不是单个校验点,而是整体链路的防御。即使某一层失效,也应该有后续隔离措施兜底。

总结

文件上传漏洞看似基础,但在真实业务里经常因为“只做了前端限制”“目录可访问”“权限没收口”而形成实际风险。比较可靠的做法是:服务端强校验、文件名去用户化、存储隔离、访问鉴权、禁用执行能力,并把异常上传场景纳入回归测试。

对于安全测试人员来说,复现时不要只盯着能否上传特殊文件,更应该把上传后的访问路径、权限边界、解析行为和二次处理链路串起来看。对于研发团队来说,上传能力最好沉淀成统一组件,避免每个业务模块各写一套不完整的实现。

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

0篇意见

推荐意见

没有意见。

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

SiteMap HACKHAT © 2026 All rights reserved.