背景文件上传是 Web 系统里很常见的功能,也是代码审计中高频出现问题的位置。头像上传、附件上传、工单图片、富文本编辑器、导入导出等模块,都可能因为校验不完整、存储路径不合理或解析配置错误,变成安全风险点。这篇文章整理一次典型代码审计中的文件上传问题分析过程,重点放在成因、审计方法和修复思路上。内容不会提供攻击利用引导,更多是帮助开发和安全同学判断一个上传模块是否可靠,以及如何把风险降下来。常见场景审计中遇到的上传逻辑通常类似下面几类:用户头像、身份证明、资质材料等图片上传。后台管理系统中的附件上传、模板导入。富文本编辑器内置上传接口。工单、论坛、评论系统中的图片或文件附件。第三方对象存储直传或后端中转上传。看起来都是普通业务功能,但只要文件类型判断、文件名处理、目录权限或 Web 容器解析规则里有一个环节出问题,就可能产生严重后果。问题成因文件上传漏洞很少是单点问题,更多是多个弱点叠加造成的。常见成因包括:只检查前端限制,没有在服务端重新校验。只根据文件后缀判断类型,没有校验文件内容。允许用户控制保存文件名或保存路径。上传目录位于 Web 可执行目录下。Web 容器对特殊后缀、双后缀、大小写后缀解析规则处理不一致。没有对上传后的文件进行重命名、隔离和权限控制。对象存储桶权限配置过宽,导致文件可被任意读取或覆盖。实际审计时,不应只看某一个 if 判断,而要把“上传入口、校验逻辑、存储位置、访问方式、执行权限、清理机制”串起来看。审计关注点代码审计时,我一般会按下面几个方向展开。1. 上传入口是否可控先确认接口是否需要认证、权限是否合理、是否存在越权上传。很多后台接口虽然设计为管理员使用,但实际只做了登录校验,没有细分权限。POST /api/upload
Cookie: session=...
[email protected]
type=avatar需要关注 type、path、module、bizId 这类参数。如果后端根据这些参数决定保存目录或文件用途,就要进一步确认是否存在目录穿越、覆盖已有文件、越权绑定业务对象等问题。2. 文件类型校验是否可信不建议仅依赖 Content-Type 或原始文件名后缀。Content-Type 来自客户端请求头,不能作为安全判断依据;后缀名也容易被伪造。更稳妥的做法是组合校验:白名单限制允许的扩展名。读取文件头特征判断真实格式。对图片类文件进行解码重编码。限制文件大小、宽高、页数等属性。拒绝可执行、脚本、压缩包内嵌危险内容等高风险类型。例如 Java 中可以使用 Apache Tika 辅助识别 MIME,但不要把识别结果当作唯一依据:String originalName = file.getOriginalFilename();
String ext = getSafeExtension(originalName);
Set<String> allowExt = Set.of(\"jpg\", \"jpeg\", \"png\", \"gif\", \"pdf\");
if (!allowExt.contains(ext)) {
throw new IllegalArgumentException(\"unsupported file type\");
}
String detectedType = tika.detect(file.getInputStream());
Set<String> allowMime = Set.of(\"image/jpeg\", \"image/png\", \"image/gif\", \"application/pdf\");
if (!allowMime.contains(detectedType)) {
throw new IllegalArgumentException(\"invalid file content\");
}这里的重点是“白名单”,而不是维护一份黑名单。黑名单很难覆盖各种解析差异和新格式。3. 文件名是否安全处理不要直接使用用户上传的原始文件名作为服务端保存文件名。原始文件名可能包含特殊字符、路径分隔符、Unicode 混淆字符,也可能造成覆盖或日志污染。推荐做法是服务端生成随机文件名,只保留经过校验的扩展名:String ext = getSafeExtension(originalName);
String newName = UUID.randomUUID().toString().replace(\"-\", \"\") + \".\" + ext;
Path baseDir = Paths.get(\"/data/app/uploads/avatar\").toAbsolutePath().normalize();
Path target = baseDir.resolve(newName).normalize();
if (!target.startsWith(baseDir)) {
throw new SecurityException(\"invalid path\");
}
Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);如果业务确实需要展示原始文件名,可以把原始文件名作为元数据保存到数据库,展示时做 HTML 转义,不要用于实际存储路径。4. 上传目录是否可执行这是很多上传风险被放大的关键点。即使文件类型校验有缺陷,如果上传目录没有执行权限,也能降低很多风险。Nginx 场景下,可以对上传目录关闭脚本解析,并增加强制下载或静态资源访问规则:location /uploads/ {
alias /data/app/uploads/;
autoindex off;
add_header X-Content-Type-Options nosniff;
}
location ~* ^/uploads/.*\\.(php|jsp|jspx|asp|aspx|cgi|pl|sh)$ {
return 403;
}如果是 PHP 应用,更建议从架构上避免把上传文件放在 Web 根目录内。由应用读取文件后返回,或者使用对象存储并配置严格的访问策略。5. 图片处理是否存在二次风险不少系统会使用 ImageMagick、GraphicsMagick、FFmpeg、LibreOffice 等组件处理上传文件。这类组件历史上出现过多次解析类漏洞。审计时除了看业务代码,也要看依赖版本和调用方式。确认处理组件版本是否仍在维护。限制输入文件大小和处理超时。避免直接拼接 shell 命令。处理任务放入低权限沙箱或容器。关闭不需要的解析协议和外部资源访问。如果需要调用命令行工具,尽量使用参数数组,避免字符串拼接:ProcessBuilder pb = new ProcessBuilder(
\"convert\",
inputPath.toString(),
\"-resize\",
\"300x300\",
outputPath.toString()
);
pb.redirectErrorStream(true);
Process process = pb.start();同时要对 inputPath、outputPath 做固定目录约束,不能来自用户直接控制。6. 对象存储配置是否过宽现在很多系统使用 S3、OSS、COS、MinIO 等对象存储。风险不只在上传接口本身,也在桶策略和临时凭证。常见问题包括:桶设置为公开读写。前端拿到的临时凭证权限过大。对象 Key 可预测,导致枚举或覆盖。没有限制上传 Content-Type、大小和路径前缀。预签名 URL 有效期过长。比较稳妥的方式是:后端根据当前用户和业务场景生成短有效期、限定路径前缀、限定操作类型的上传凭证。上传完成后再由后端回调校验文件元数据,确认归属和类型。关键加固建议一个相对可靠的上传模块,至少应满足下面这些要求:上传接口必须有认证和业务权限校验。服务端使用白名单校验扩展名和内容类型。文件大小、数量、频率都要有限制。服务端生成随机文件名,用户不能控制保存路径。上传目录不具备脚本执行能力。图片、文档等复杂格式处理放到低权限环境中。上传后的访问权限与业务权限一致,避免只要知道 URL 就能访问敏感文件。记录上传日志,包括用户、IP、业务对象、文件哈希、大小和检测结果。对历史文件建立清理机制,避免临时文件长期堆积。如果业务涉及敏感材料,还应考虑病毒扫描、敏感信息识别、访问水印和下载审计。一个推荐的处理流程可以把上传流程拆成几个固定阶段,避免校验逻辑散落在各个业务模块里:1. 鉴权与业务权限校验
2. 读取文件元信息
3. 校验大小、数量、扩展名
4. 校验真实内容类型
5. 生成随机文件名
6. 保存到隔离目录或对象存储
7. 可选:异步扫描、转码、缩略图生成
8. 写入数据库元数据
9. 返回文件 ID,而不是直接返回物理路径返回文件 ID 的好处是后续访问可以统一走下载接口,便于做权限判断、审计和防盗链,而不是把真实存储路径暴露给前端。代码审计中的排查技巧实际审计时,可以从以下关键词入手定位上传逻辑:MultipartFile
getOriginalFilename
transferTo
move_uploaded_file
$_FILES
Request.Files
IFormFile
FormFile
upload
attachment
editor
ueditor
kindeditor
file_put_contents
Files.copy定位到代码后,不要只看当前函数,还要继续追踪:保存路径从哪里来。文件名是否经过安全处理。校验函数是否所有上传入口都调用了。是否存在旧接口、调试接口、编辑器默认接口。上传后的文件是否可以直接访问。反向代理或 Web 容器是否会对该目录做脚本解析。有时业务代码看起来没有明显问题,但 Nginx、Apache、Tomcat 或 PHP-FPM 的配置会改变风险判断。所以审计报告里最好同时给出代码问题和部署侧建议。注意点不要依赖前端校验。前端限制只能改善用户体验,不能作为安全边界。不要把 Content-Type 当成可信输入。不要使用用户原始文件名落盘。不要让上传目录具备执行权限。不要忽略富文本编辑器和历史遗留上传接口。不要把对象存储桶设置成公开写。不要在日志或响应中暴露服务器真实路径。总结文件上传漏洞的本质不是“能不能上传某个后缀”,而是系统有没有把不可信文件放进可执行、可覆盖、可访问的危险位置。审计时要从入口、校验、存储、访问、解析和权限几个维度一起看。比较稳的设计原则是:上传文件一律按不可信数据处理;类型使用白名单;文件名由服务端生成;存储目录与执行环境隔离;访问统一经过权限控制。只要这些基础措施做到位,即使某个校验点出现疏漏,也不至于直接扩大成高危问题。
推荐意见