跳转到帖子

背景文件上传是 Web 系统里很常见的功能,头像、附件、工单截图、文档导入都会用到。也正因为入口常见、数据不可控,上传点一直是代码审计和渗透测试中优先关注的位置。这篇文章结合我在审计业务系统时经常遇到的问题,整理一套相对完整的分析思路和防护链路。重点不是讲“如何利用”,而是说明为什么某些写法不可靠,以及在工程上如何把风险压下去。常见问题场景很多上传漏洞并不是单点失误,而是多个环节同时薄弱造成的。常见场景包括:仅通过前端限制文件后缀,后端没有重新校验。后端只判断文件名后缀,没有校验 MIME、文件头、内容特征。上传目录位于 Web 可访问路径下,并且服务器可能解析动态脚本。文件名直接使用用户输入,导致路径穿越、覆盖文件或特殊字符问题。图片处理、压缩、解压等二次处理逻辑缺少边界控制。权限控制缺失,普通用户可以上传到全站公共目录。在真实项目中,最危险的情况通常是“上传目录可访问 + 文件类型校验薄弱 + Web 容器存在错误解析配置”。这类组合风险需要从代码和部署两侧一起看。技术分析先看一段比较典型的问题代码,示例使用 Java Spring 风格,其他语言也类似。public String upload(MultipartFile file) throws IOException {
String originalName = file.getOriginalFilename();

if (!originalName.endsWith(\".jpg\") && !originalName.endsWith(\".png\")) {
throw new RuntimeException(\"invalid file type\");
}

Path target = Paths.get(\"/var/www/html/upload/\" + originalName);
file.transferTo(target);

return \"/upload/\" + originalName;
}这段代码看起来做了后缀校验,但实际存在多个问题:后缀判断过于简单:大小写、双后缀、特殊字符、编码差异都可能导致判断结果和服务器解析结果不一致。使用原始文件名:如果文件名包含路径分隔符、控制字符或同名文件,可能引入额外风险。上传目录在 Web 根目录:用户上传内容可以直接被访问,一旦解析配置异常,影响会放大。缺少内容校验:只看文件名无法确认文件真实类型。缺少大小限制:容易被大文件、压缩炸弹或大量小文件拖垮存储资源。安全的上传处理应该是多层校验和隔离,不依赖单个判断条件。一个更合理的处理思路包括:白名单类型、随机文件名、非 Web 目录存储、内容检测、权限控制、统一下载接口和日志审计。关键审计步骤1. 定位上传入口代码审计时可以先通过关键词定位上传逻辑,例如:MultipartFile
Request.Files
move_uploaded_file
FormFile
FileUpload
transferTo
SaveAs
writeFile除了显式上传接口,也要关注导入、富文本编辑器、客服附件、插件市场、压缩包解压、头像裁剪等功能。这些入口经常被遗漏。2. 检查后端校验逻辑后端至少应检查以下内容:文件大小是否有限制。扩展名是否使用严格白名单。MIME 是否仅作为辅助判断,而不是唯一依据。是否读取文件头或通过可靠库识别类型。图片类文件是否经过重新编码处理。压缩包是否限制文件数量、总大小和解压路径。对于图片上传,常见做法是使用图像库重新解码再编码,丢弃原始文件中的非图像结构。示例:BufferedImage image = ImageIO.read(file.getInputStream());
if (image == null) {
throw new RuntimeException(\"invalid image\");
}

String safeName = UUID.randomUUID().toString() + \".jpg\";
Path target = storageDir.resolve(safeName).normalize();

ImageIO.write(image, \"jpg\", target.toFile());这类处理不能覆盖所有格式问题,但相比直接保存用户原始内容,风险会明显降低。3. 检查文件名处理不建议使用用户上传的原始文件名作为存储文件名。推荐做法是服务端生成随机名称,并单独保存原始文件名用于展示。String ext = getSafeExtension(originalName);
String storedName = UUID.randomUUID().toString().replace(\"-\", \"\") + ext;

Path target = storageDir.resolve(storedName).normalize();
if (!target.startsWith(storageDir)) {
throw new RuntimeException(\"invalid path\");
}这里有两个细节:normalize() 之后必须判断是否仍在预期目录内,防止路径穿越。扩展名必须从白名单映射得出,不要直接信任原始后缀。4. 检查存储位置和访问方式上传文件最好存储在 Web 根目录之外,通过受控接口下载或预览。这样即使某个文件校验漏掉,也不会被 Web 容器直接解析。/data/app-upload/
├── avatar/
├── attachment/
└── temp/访问时由应用层读取文件并设置响应头:Content-Type: image/jpeg
Content-Disposition: inline; filename=\"avatar.jpg\"
X-Content-Type-Options: nosniff如果业务必须使用静态资源服务器,也应确保上传目录禁用脚本解析,只允许作为普通静态文件返回。location /uploads/ {
root /data/static;
autoindex off;
default_type application/octet-stream;
add_header X-Content-Type-Options nosniff;
}不同服务器配置方式不同,核心原则是:上传目录不要具备动态执行能力。5. 检查权限与业务边界很多上传点的问题不在文件格式,而在权限边界。例如:用户 A 能否访问用户 B 上传的私有附件。低权限用户能否上传全站公告图片。临时文件是否长期公开可访问。未登录用户是否能无限制上传。上传后是否存在越权替换、删除或引用其他文件的能力。建议上传接口和文件访问接口都做权限校验,不要认为“文件名随机”就等于安全。6. 关注二次处理风险上传后常见的二次处理包括图片压缩、文档预览、压缩包解压、音视频转码、OCR 识别等。这些组件本身也可能存在安全风险。审计时可以重点关注:调用外部命令时是否拼接用户输入。处理进程是否有超时、内存和 CPU 限制。临时目录是否隔离。解析库版本是否长期未更新。失败文件是否仍被保留并可访问。如果使用命令行工具,不要把用户可控参数直接拼进字符串命令,优先使用参数数组方式调用。ProcessBuilder pb = new ProcessBuilder(
\"convert\",
inputPath.toString(),
\"-resize\",
\"800x800\",
outputPath.toString()
);
pb.start();推荐防护链路实际落地时,可以按下面这条链路设计:入口限制:认证、权限、频率限制、大小限制。类型白名单:基于业务只允许必要类型,不开放泛文件上传。内容识别:扩展名、MIME、文件头、解析库综合判断。安全命名:服务端生成随机文件名,原始名称仅用于展示。目录隔离:存储在非 Web 根目录,上传目录无执行权限。二次处理:图片重编码、文档转换沙箱化、压缩包安全解压。访问控制:下载和预览走统一接口,按业务权限校验。审计监控:记录上传者、文件类型、大小、访问行为和处理结果。压缩包上传的额外注意点压缩包导入功能经常被低估。除了文件类型,重点还要处理 Zip Slip、解压炸弹和大量小文件问题。Path destDir = Paths.get(\"/data/import/workdir\").toAbsolutePath().normalize();

for (ZipEntry entry : entries) {
Path resolved = destDir.resolve(entry.getName()).normalize();

if (!resolved.startsWith(destDir)) {
throw new RuntimeException(\"invalid zip entry path\");
}

if (entry.getSize() > MAX_SINGLE_FILE_SIZE) {
throw new RuntimeException(\"file too large\");
}

// 继续处理白名单文件类型
}还需要限制:压缩包总解压大小。文件数量。目录层级。单个文件大小。允许的文件后缀。测试与验证建议在授权测试或自测环境中,可以重点验证以下点:后端是否独立校验,不依赖前端。非法扩展名是否被拒绝。大小写后缀是否按预期处理。上传文件是否能被直接访问。上传目录是否具备脚本解析能力。文件名中特殊字符是否被安全处理。压缩包中异常路径是否被拒绝。接口是否存在未授权上传或越权访问。测试结果不要只停留在“能不能上传成功”,还要看文件最终存在哪里、以什么 Content-Type 返回、是否经过业务权限校验、失败文件是否残留。常见误区只做前端限制:前端校验只能改善体验,不能作为安全边界。只校验 MIME:MIME 可以被客户端声明,不可靠。黑名单过滤:后缀和解析规则太多,黑名单容易遗漏。随机文件名即安全:随机名不能替代类型校验和访问控制。对象存储天然安全:错误的 Bucket 权限和回源配置同样会造成风险。总结文件上传安全的关键不是某一个校验函数,而是一整条链路:入口控制、类型识别、存储隔离、执行权限收敛、访问鉴权和日志审计都要到位。从代码审计角度看,建议优先关注三个问题:文件是否直接落到 Web 可访问目录、后端是否只依赖后缀判断、访问接口是否存在权限缺失。只要这三点处理不当,上传功能的风险通常就不会低。经验上,上传功能最稳妥的设计是:用户上传的原始内容永远不要直接进入可执行环境,业务需要展示时由受控接口读取并返回。

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

0篇意见

推荐意见

没有意见。

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