跳转到帖子

背景文件上传是 Web 系统里很常见的功能:头像、附件、导入文件、工单截图、富文本图片等都会涉及。它看起来只是一个业务功能,但在实际审计和应急里,上传点经常是漏洞链的入口。比较典型的风险包括:上传可执行脚本、路径穿越覆盖文件、解析差异导致脚本执行、图片二次渲染缺失导致恶意内容保留、对象存储权限配置不当,以及上传后的文件被业务再次处理时触发解析漏洞。这篇文章不讨论攻击利用细节,而是从代码审计和防护落地的角度,整理一套比较稳妥的检查思路。适合在做 Web 安全评审、上线前审计、历史系统加固时作为参考清单。常见问题场景审计上传功能时,我通常不会只看“是否限制后缀”。真实项目里,很多风险是多点叠加造成的。下面这些场景比较常见:只在前端限制文件类型,后端没有严格校验。后端仅通过文件后缀判断类型,未检查 MIME、文件头和实际内容。上传目录位于 Web 可访问路径下,并且服务器可能解析动态脚本。文件名直接使用用户输入,存在路径穿越、覆盖、特殊字符解析问题。允许用户控制保存路径、回调地址、对象存储 key。图片上传后没有重新编码,原始文件被直接保存。导入类功能会对上传文件进行解压、解析 XML、解析 Office 或 CSV,产生二次风险。历史环境里 Nginx、Apache、PHP-FPM、Tomcat 等解析规则不一致,导致配置层风险。文件上传的核心防护思路不是“拦住某个后缀”,而是:限制入口、隔离存储、不可执行、重新生成、最小权限、可审计。技术分析:上传链路应该看哪些点1. 前端校验不能作为安全边界前端的 accept 属性、JavaScript 文件类型判断只能改善用户体验,不能作为安全控制。审计时重点看服务端逻辑。一个比较基础的服务端校验至少应包含大小限制、类型白名单、文件名重写、存储路径固定、异常处理。错误示例思路通常是:直接使用原始文件名、直接拼接路径、根据后缀决定是否允许上传。这类逻辑在长期维护中很容易被绕过或引入兼容性问题。2. 类型判断要采用白名单组合校验比较推荐的做法是白名单策略,而不是黑名单策略。比如业务只允许头像,就只允许 jpeg、png、webp 等明确类型;业务只允许 PDF,就只允许 PDF。不要因为“以后可能要支持更多格式”而放开范围。服务端可以组合检查以下信息:扩展名:只允许业务需要的后缀。MIME:作为辅助参考,不能单独信任。文件魔数:检查文件头特征。内容解析:对图片进行解码确认,对 PDF 或文档使用可靠库解析。大小与尺寸:避免超大文件或异常图片造成资源消耗。例如 Java 场景可以用 Apache Tika 辅助识别类型,但仍建议结合业务白名单:Set<String> allowTypes = Set.of(\"image/jpeg\", \"image/png\", \"image/webp\");

String detectedType = tika.detect(inputStream);
if (!allowTypes.contains(detectedType)) {
throw new IllegalArgumentException(\"unsupported file type\");
}需要注意,自动识别库本身也不是绝对边界。真正稳妥的方式是:识别通过后,对图片类文件进行重新编码,生成新的文件内容,而不是直接保存用户原始文件。3. 文件名必须由服务端生成文件名不要直接使用用户输入。原始文件名最多作为展示字段保存到数据库,不应该参与真实存储路径。原因包括路径穿越、覆盖已有文件、特殊字符兼容问题、日志污染、不同文件系统解析差异等。推荐做法是使用随机 ID 或哈希生成存储名,并固定目录层级:String ext = resolveSafeExtension(detectedType);
String objectName = UUID.randomUUID().toString().replace(\"-\", \"\") + ext;
Path savePath = baseDir.resolve(objectName).normalize();

if (!savePath.startsWith(baseDir)) {
throw new SecurityException(\"invalid path\");
}这里的 normalize 和 startsWith 检查很重要,特别是当路径中存在用户可控片段时。如果业务必须支持目录分类,也建议目录名来自服务端枚举,而不是用户任意传入。4. 上传目录不要具备脚本执行能力从防护角度看,上传文件最好放在不可执行区域。更进一步,上传文件可以放在对象存储、独立文件服务或非 Web 根目录,然后通过受控接口读取。如果使用 Nginx 提供静态访问,可以明确关闭上传目录的脚本解析能力,并限制访问行为。示例仅表达加固思路,具体路径需要结合实际环境调整:location /uploads/ {
root /data/app-static;
autoindex off;
types {
image/jpeg jpg jpeg;
image/png png;
image/webp webp;
application/pdf pdf;
}
default_type application/octet-stream;
add_header X-Content-Type-Options nosniff;
}如果后端存在 PHP、JSP、ASP.NET 等动态解析环境,更要避免上传目录与动态解析规则重叠。历史项目里经常出现“业务静态目录”和“脚本解析 location”匹配顺序不清晰的问题,建议通过配置审计确认最终命中规则。5. 图片类文件建议重新编码头像、截图、富文本图片这类业务,比较有效的方式是服务端重新解码并编码输出。这样可以丢弃原始文件中的附加内容、异常块、元数据等。示例思路如下:BufferedImage image = ImageIO.read(uploadInputStream);
if (image == null) {
throw new IllegalArgumentException(\"invalid image\");
}

if (image.getWidth() > 4096 || image.getHeight() > 4096) {
throw new IllegalArgumentException(\"image too large\");
}

BufferedImage output = new BufferedImage(
image.getWidth(),
image.getHeight(),
BufferedImage.TYPE_INT_RGB
);

Graphics2D g = output.createGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();

ImageIO.write(output, \"jpg\", targetFile);注意点是:图片库要及时更新,避免解析库自身漏洞;对超大尺寸、异常压缩比、动图帧数也要限制,防止资源消耗问题。6. 压缩包和导入功能要单独审计很多系统并不把“导入文件”视为上传风险,但从安全角度看,Excel、CSV、XML、ZIP、DOCX 都可能带来解析侧风险。压缩包处理要重点关注:解压后的总大小限制,避免压缩炸弹。文件数量限制,避免大量小文件耗尽资源。解压路径校验,避免路径穿越。禁止解压出可执行脚本或危险扩展。解压目录使用临时隔离目录,处理完成后清理。路径校验可以参考下面的模式:Path targetDir = Paths.get(\"/data/tmp/import\").toAbsolutePath().normalize();

for (ZipEntry entry : entries) {
Path output = targetDir.resolve(entry.getName()).normalize();

if (!output.startsWith(targetDir)) {
throw new SecurityException(\"zip entry path traversal detected\");
}

// 继续处理大小、数量、扩展名等限制
}CSV 导入还要注意公式注入问题。如果导出的 CSV 会被办公软件打开,应对以 =、+、-、@ 开头的字段做处理,避免后续使用场景产生风险。7. 对象存储不是天然安全现在很多系统会把文件上传到 S3、OSS、COS、MinIO 等对象存储。对象存储能降低 Web 宿主机被写入脚本的风险,但不代表没有安全问题。常见配置问题包括:Bucket 公共读写权限过大。上传凭证权限范围过宽,允许覆盖任意对象。对象 key 可预测,导致越权访问或枚举。回调接口未校验签名。直接信任客户端上报的文件类型和大小。更合理的做法是:服务端生成短时上传凭证,限制前缀、大小、类型和有效期;上传完成后由服务端进行二次校验;业务访问文件时通过鉴权接口或短时签名 URL 暴露。关键步骤:一次上传点审计清单实际做代码审计时,可以按下面的顺序走,效率会比较高:定位所有上传入口:普通上传、富文本、头像、工单附件、导入、API 上传、移动端接口。确认服务端是否校验:大小、类型、扩展名、文件头、解析结果。确认文件名生成方式:是否使用用户原始文件名,是否存在路径拼接。确认存储位置:是否在 Web 根目录,是否可被动态解析。确认访问方式:是否需要鉴权,是否存在越权读取。确认后处理逻辑:是否解压、转码、OCR、解析 XML、调用外部工具。确认异常和日志:是否记录原始文件名,是否可能污染日志或暴露路径。确认清理机制:临时文件是否删除,失败上传是否残留。确认部署配置:Nginx、Apache、Tomcat、对象存储权限是否与代码假设一致。防护参考实现思路下面是一段偏伪代码的上传处理流程,重点是体现防护链路,而不是绑定某个框架:1. 接收文件流
2. 检查文件大小是否在业务允许范围内
3. 读取有限长度内容用于类型识别
4. 校验扩展名、MIME、文件魔数和业务类型白名单
5. 对图片类文件执行解码和重新编码
6. 由服务端生成随机文件名
7. 保存到不可执行目录或对象存储受限前缀
8. 写入数据库记录:文件ID、原始展示名、真实存储名、大小、类型、上传人
9. 访问文件时通过文件ID鉴权,不直接暴露内部路径
10. 定期清理孤儿文件和临时文件如果业务对安全要求较高,可以把上传处理放到异步队列中,先进入隔离区,经过扫描、转码、审核后再进入正式可访问区。这种方式对成本有要求,但在办公协同、客服系统、内容平台中比较实用。注意点不要依赖黑名单。危险后缀、解析规则和中间件行为会变,黑名单长期维护成本很高。不要信任 Content-Type。它来自客户端请求头,只能作为辅助信息。不要把上传目录交给动态语言解析器处理。配置层隔离比代码判断更可靠。不要忽视历史文件。加固代码后,旧文件仍可能存在风险,需要做存量扫描。不要忽视二次处理。很多漏洞不是上传瞬间触发,而是在预览、转换、解压、导入时触发。不要把对象存储当成简单文件夹。权限、签名、回调校验都需要单独设计。总结文件上传漏洞之所以反复出现,是因为它横跨业务代码、文件系统、中间件配置、对象存储和后处理组件。只修一个点,往往不能真正解决问题。比较可靠的策略是建立完整链路:入口白名单校验、服务端生成文件名、不可执行存储、必要时重新编码、访问鉴权、部署配置隔离、日志审计和存量清理。这样即使某个环节出现偏差,也不至于直接扩大成高危问题。在做代码审计时,建议把上传点作为独立专题梳理。尤其是老系统、富文本编辑器、批量导入、压缩包处理和对象存储直传,都是值得优先看的位置。

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

0篇意见

推荐意见

没有意见。

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