背景
文件上传是 Web 系统里非常常见的功能,头像、附件、工单截图、导入模板都会涉及。它看起来只是业务能力,但在安全审计里经常是高风险入口:一旦校验逻辑不完整,可能引发任意文件写入、存储型 XSS、服务端解析异常、敏感文件覆盖等问题。
这篇文章整理一次比较典型的文件上传风险排查思路,重点放在代码审计和防护设计上。内容不会涉及未授权入侵或攻击利用细节,更多是从研发和安全评审角度,说明如何判断一个上传功能是否可靠。
常见场景
在实际项目里,文件上传通常存在几类实现方式:
- 后端应用直接接收文件并保存到本地目录。
- 后端接收后转存到对象存储,例如 S3、OSS、COS、MinIO。
- 前端直传对象存储,后端只负责签发上传凭证。
- 上传后进入异步处理流程,例如图片压缩、文档解析、病毒扫描。
不同实现方式风险点不完全一样,但核心问题基本围绕三个方面:文件内容是否可信、文件路径是否可控、文件访问是否安全。
问题分析
审计上传功能时,不建议只看扩展名判断。扩展名是用户可控输入,只能作为辅助信息。更稳妥的方式是从以下几个维度交叉验证。
一、文件类型校验不能只依赖后缀
很多历史代码会这样判断:
String filename = file.getOriginalFilename();
if (filename.endsWith(".jpg") || filename.endsWith(".png")) {
file.transferTo(new File(uploadDir, filename));
}这个逻辑的问题比较明显:文件名完全来自客户端,攻击者可以构造特殊名称;同时,后缀无法证明文件内容是真正的图片。
更可靠的做法是至少同时检查 MIME、文件头特征和实际解码结果。以图片为例,可以用标准图片库尝试解析,并限制宽高、大小、格式。
BufferedImage image = ImageIO.read(file.getInputStream());
if (image == null) {
throw new IllegalArgumentException("invalid image");
}
int width = image.getWidth();
int height = image.getHeight();
if (width <= 0 || height <= 0 || width > 8000 || height > 8000) {
throw new IllegalArgumentException("invalid image size");
}注意,MIME 头也来自请求,不应单独作为信任依据。它的价值更多是辅助判断和记录日志。
二、文件名和路径必须重新生成
上传保存时,最容易被忽略的是路径拼接。以下写法在代码审计中比较常见:
File target = new File(uploadDir + "/" + file.getOriginalFilename());
file.transferTo(target);这里存在两个问题:一是原始文件名可能包含路径分隔符;二是可能导致文件覆盖。安全做法是服务端生成随机文件名,并将用户原始文件名仅作为展示字段保存到数据库。
String ext = detectSafeExtension(file);
String safeName = UUID.randomUUID().toString().replace("-", "") + ext;
Path basePath = Paths.get(uploadDir).toAbsolutePath().normalize();
Path targetPath = basePath.resolve(safeName).normalize();
if (!targetPath.startsWith(basePath)) {
throw new IllegalArgumentException("invalid upload path");
}
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);这里的关键点是 normalize 后再检查 startsWith,避免路径穿越导致文件落到预期目录之外。
三、上传目录不要具备脚本执行能力
如果上传目录位于 Web 根目录下,并且应用服务器会解析其中某些脚本类型,那么风险会被放大。更推荐的方式是:
- 上传文件存储在 Web 根目录之外。
- 通过受控下载接口读取文件。
- 对象存储桶默认私有,访问通过短期签名 URL 或后端代理。
- 静态资源域名与主站域名隔离,降低 XSS 影响面。
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)$ {
return 403;
}如果业务允许公开访问图片,也建议设置准确的 Content-Type,并加上 X-Content-Type-Options: nosniff,避免浏览器进行内容嗅探。
四、大小、数量和解压类文件要做限制
上传功能不仅有代码执行类风险,也有资源消耗风险。例如超大文件、批量上传、压缩包炸弹、图片解析消耗过高等。实际防护里建议加入:
- 单文件大小限制。
- 单用户或单 IP 上传频率限制。
- 总存储配额限制。
- 图片宽高和像素总量限制。
- 压缩包解压后的文件数量、总大小、目录层级限制。
对于 ZIP 处理,不能简单相信压缩前大小。需要边解压边统计,超过阈值立刻中断。
long totalSize = 0;
int fileCount = 0;
while ((entry = zipInputStream.getNextEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
fileCount++;
if (fileCount > 1000) {
throw new IllegalArgumentException("too many files");
}
long written = copyWithLimit(zipInputStream, outputStream, 10 * 1024 * 1024);
totalSize += written;
if (totalSize > 100 * 1024 * 1024) {
throw new IllegalArgumentException("zip too large");
}
}五、文件下载接口也要一起审计
很多时候上传保存本身没有明显问题,但下载接口存在越权或任意文件读取。常见风险包括:
- 通过 fileId 下载时没有校验当前用户权限。
- 通过 path 参数读取文件,导致路径穿越。
- 返回文件时 Content-Disposition 处理不当,引发响应头注入。
- 私有文件使用永久公开 URL,无法撤销访问。
较稳妥的做法是下载接口只接收文件 ID,通过数据库查询真实存储路径,并校验业务权限。
Attachment attachment = attachmentService.getById(fileId);
if (attachment == null || !permissionService.canRead(currentUser, attachment)) {
throw new AccessDeniedException("no permission");
}
Path filePath = storage.resolve(attachment.getStorageName());
return download(filePath, attachment.getOriginalName());关键审计步骤
实际排查时,可以按下面的顺序进行,效率会比较高。
- 定位所有上传入口,包括后台、接口、富文本、导入功能和移动端接口。
- 确认文件最终保存位置,是本地目录还是对象存储。
- 检查文件名是否由服务端生成,是否存在路径拼接。
- 检查类型校验是否只依赖后缀或 Content-Type。
- 检查上传目录是否可被 Web 服务器解析执行。
- 检查是否存在大小、频率、配额限制。
- 检查下载接口是否有权限校验和路径约束。
- 查看日志中是否记录上传人、文件 ID、大小、类型、存储路径、处理结果。
安全基线建议
| 检查项 | 建议做法 |
| 文件名 | 服务端随机生成,原始文件名仅用于展示 |
| 保存路径 | 固定目录,normalize 后校验路径边界 |
| 类型判断 | 后缀、文件头、解析结果多重校验 |
| 访问控制 | 私有文件必须鉴权,避免永久公开链接 |
| 执行权限 | 上传目录禁止脚本执行,尽量放在 Web 根目录外 |
| 资源限制 | 限制大小、数量、频率、解压后总量 |
| 日志审计 | 记录上传主体、文件信息、处理结果和异常原因 |
注意点
上传安全不是一个 if 判断能解决的问题,而是一组边界控制:入口校验、存储隔离、访问鉴权、解析限制和日志审计都要同时考虑。
- 不要把前端校验当作安全措施,前端只能改善用户体验。
- 不要信任原始文件名、MIME、后缀、压缩包内路径。
- 不要让上传文件和应用代码处在同一执行上下文。
- 涉及文档预览、图片处理时,要关注第三方解析库漏洞和补丁更新。
- 如果有杀毒或内容检测流程,建议异步处理并设置隔离状态,未检测通过前不要公开访问。
总结
文件上传风险的根源在于“把不可信内容带入服务端存储和解析流程”。审计时不能只看能不能上传某个后缀,更要看文件从进入系统到被访问、被解析、被下载的完整链路。
比较可靠的方案是:服务端生成文件名,固定存储目录,做内容级校验,上传目录不具备执行能力,下载接口按业务权限控制,并对资源消耗做上限约束。这样即使某个单点校验出现遗漏,也不会轻易演变成高危问题。
推荐意见