跳转到帖子

文件上传功能看似只是把一个文件保存到服务器,但它同时涉及文件名处理、内容校验、存储位置、访问控制、解析器行为和部署配置等多个边界。实际审计中,很多问题并不来自某一行明显的危险代码,而是来自“校验了一部分、信任了另一部分”的组合。本文以防守和修复验证为主线,整理一次文件上传功能的审计方法。示例使用抽象代码和本地测试思路,不涉及对真实系统的攻击操作。一、问题场景与风险边界典型上传接口通常包含以下流程:接收 multipart/form-data 请求。读取原始文件名、扩展名和 MIME 类型。检查大小、后缀或文件头。生成保存路径并写入磁盘或对象存储。返回文件 URL,供用户后续访问。风险主要集中在四个方面:文件内容被当作脚本、模板或配置解析,形成远程代码执行风险。上传文件覆盖已有文件,导致配置、缓存或业务数据被破坏。通过路径拼接影响保存位置,形成路径穿越或任意文件写入。上传后的文件缺少访问控制,导致敏感文档越权读取。审计时不要只问“扩展名是否限制”,还要问“文件最终被谁解析、保存在哪里、谁可以访问、生命周期如何管理”。二、审计的第一步:画出数据流建议先从控制器或路由入口开始,沿着数据流跟踪以下变量:原始文件名:通常来自客户端,不能作为可信路径组件。Content-Type:由客户端控制,只能作为辅助信号。文件内容:需要基于实际字节进行校验,必要时调用可靠的解析库。目标文件名:应由服务端生成,避免继承用户输入。保存目录:应来自固定配置,并在启动时校验其权限和位置。返回地址:需要经过统一的访问控制和下载策略。在代码审计中,可以优先搜索以下关键词,再反向确认调用链:getOriginalFilename
getContentType
transferTo
Files.write
FileOutputStream
path.join
resolve
sendFile
download
static resource mapping搜索结果只是入口,关键是确认变量是否经过了规范化、验证和权限控制。尤其要注意同一个文件可能先被安全目录保存,之后又被异步任务移动到可执行目录。三、常见缺陷一:只校验扩展名或 MIME 类型扩展名和 MIME 类型都可以被客户端修改,不能单独作为文件类型判断依据。一个相对稳妥的策略是分层校验:业务层限制允许的文件类型和大小。服务端根据文件头或解析库判断真实类型。对图片、压缩包、文档等格式分别使用对应解析器。必要时进行转码或重新生成,避免直接信任原始内容。例如,图片上传不应只判断文件名是否以 .jpg 结尾,而应尝试使用图像库解码,并在解码成功后重新编码为服务端认可的格式。重新编码还能去除部分非必要元数据,降低解析器差异带来的风险。需要注意的是,文件头检查不是万能的。多格式文件、解析器兼容差异以及后续处理链都可能产生新的风险。因此,类型校验必须和存储隔离、下载响应头及权限控制一起设计。四、常见缺陷二:路径拼接与文件名信任下面是一个不推荐的简化示例。问题不在于使用了某个特定语言,而在于服务端直接信任用户提供的文件名:String name = upload.getOriginalFilename();
Path target = Paths.get(uploadDir, name);
upload.transferTo(target.toFile());这里至少存在几个审计疑点:name 可能包含路径分隔符或特殊路径片段。同名文件可能覆盖已有内容。目标目录是否位于静态资源目录中并不明确。没有体现大小、类型和内容校验。更合理的做法是让原始文件名只作为展示信息保存,实际落盘名称由服务端生成。路径也应经过规范化并确认仍位于预期目录内:String originalName = upload.getOriginalFilename();
String extension = extensionOf(originalName);
validateExtension(extension);
validateSize(upload.getSize());
validateContent(upload.getInputStream(), extension);

String storedName = randomIdentifier() + normalizedExtension(extension);
Path base = Paths.get(configuredUploadDir).toAbsolutePath().normalize();
Path target = base.resolve(storedName).normalize();

if (!target.startsWith(base)) {
throw new SecurityException("invalid storage path");
}

save(upload, target);
recordMetadata(originalName, storedName, currentUserId());其中,randomIdentifier 应使用可靠的随机标识生成方式;normalizedExtension 只能从服务端认可的枚举中选择,而不是简单返回用户输入的字符串。五、常见缺陷三:上传目录与解析目录混用即使文件类型校验做得比较完整,也不建议把用户上传目录放在应用代码目录、模板目录、脚本目录或可执行目录下。更稳妥的存储策略包括:上传目录与应用部署目录分离。上传目录不允许脚本执行,不参与模板解析。应用进程只拥有必要的读写权限,不使用过高系统权限。对外下载通过受控接口完成,而不是直接暴露物理目录。优先使用对象存储,并关闭不必要的公共读权限。在反向代理或应用服务器配置中,也要检查静态资源映射、脚本处理规则和错误页面回退逻辑。某些部署环境会把未知扩展名交给上游解析,导致“上传目录不可执行”的设计在配置层失效。六、访问控制:上传安全不等于下载安全很多系统只关注“谁能上传”,忽略了“谁能读取”。对上传文件的访问应至少回答以下问题:文件是否属于某个用户、项目或租户?下载请求是否重新执行授权检查?文件 URL 是否包含可预测的自增编号?删除、替换和分享操作是否有独立权限?管理员、普通用户和服务账号看到的内容是否一致?推荐使用业务对象 ID 与存储对象 ID 分离的方式。前端拿到的可以是业务层标识,服务端根据当前会话查询授权关系,再映射到实际存储对象。不要把物理路径、内部桶名或未经授权的对象键直接返回给客户端。七、修复验证:用测试证明边界真的生效修复后不能只看代码差异,至少应覆盖以下测试类别:合法文件:允许的类型、大小和编码能够正常上传和下载。边界大小:刚好达到限制值、超过限制值时行为一致且可预期。伪造类型:扩展名、MIME 和实际内容不一致时应拒绝。特殊文件名:包含空格、Unicode 字符、路径分隔符和超长名称时不会影响保存路径。同名上传:不会覆盖其他用户或已有对象。越权下载:修改业务对象标识后不能读取无权限文件。异常中断:上传过程中断不会留下可被直接访问的半成品文件。在自动化测试中,可以使用临时目录和模拟存储,验证最终路径与权限结果,而不是向生产目录写入测试文件。下面是一个适合放在单元测试或集成测试中的检查思路:assert upload(validImage).status == 200
assert upload(fileWithMismatchedContent).status == 400
assert upload(fileExceedingLimit).status == 413
assert upload(nameContainingPathSeparators).storedOutsideOriginalName == true
assert download(fileOwnedByOtherUser).status in [403, 404]
assert storedObjectName != originalFilename此外,还应检查日志是否记录了用户、业务对象、文件大小、校验结果和失败原因,但不要把文件内容、访问令牌或敏感个人信息直接写入日志。八、代码审计中的几个容易遗漏的点异步处理链:上传接口本身安全,但消息队列消费者可能把文件移动到高风险目录。压缩包解压:需要防止解压路径越界,并限制文件数量、总大小和递归层级。缩略图生成:图像解析器和媒体处理工具应及时更新,并限制资源消耗。文件替换:更新操作不能只校验新增文件,旧对象的删除和新对象的绑定也要有权限检查。错误处理:不要把服务器绝对路径、解析器堆栈或对象存储内部信息返回给客户端。备份与临时目录:临时文件、失败文件和备份文件同样可能被静态服务暴露。九、推荐的防守清单使用服务端生成的不可预测文件名,原始文件名仅作为元数据保存。使用允许列表限制扩展名、媒体类型和文件大小。对关键格式进行真实解析或转码,不依赖单一请求头。将上传目录置于应用和脚本执行目录之外。下载时执行对象级授权检查,并设置合适的 Content-Disposition 与 Content-Type。对路径进行规范化,并确认目标路径仍位于固定根目录之下。限制上传、下载、解压和转换过程的资源消耗。使用自动化测试覆盖异常输入、越权访问和中断场景。定期更新文件解析库,并对高风险处理链进行隔离。总结文件上传漏洞的本质,是不可信数据跨越了输入、存储、解析或访问控制边界。单独增加一个后缀判断通常只能解决很小的一部分问题。完整的防护应当从数据流出发,分别确认文件内容、文件名、存储路径、解析环境和访问权限。在实际项目中,建议把上传功能当作一个独立的安全模块设计:输入阶段负责校验,存储阶段负责隔离,访问阶段负责授权,处理阶段负责资源限制,运维阶段负责日志和监控。这样即使某一层出现遗漏,也能尽量避免问题直接扩大为任意文件写入、代码执行或数据越权。

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

0篇意见

推荐意见

没有意见。

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