CyberSecurity之什么是文件存储型XSS攻击

文件存储型XSS是一种利用文件上传功能实施的持久化攻击。MIME校验是防御此类攻击的重要手段,但其正确实施方式并非简单地检查MIME类型,而是需要对Content-Type头进行严格的解析和验证。

🎯 什么是文件存储型XSS攻击?

这是一种存储型XSS(持久型XSS) 攻击。攻击者将包含恶意脚本的文件上传到目标服务器,当其他用户访问或预览该文件时,恶意脚本便会在其浏览器中执行。其核心危害在于脚本被"存储"在服务器上,影响所有访问相关资源的用户。

常见攻击方式包括

  • 利用SVG图片 :在SVG文件中嵌入<script>标签,当浏览器渲染该"图片"时脚本即被执行,可用于窃取Cookie或凭证。

  • 伪装文件名 :通过双重扩展名(如malicious.svg.png)绕过前端限制。

  • 利用文档文件:在允许上传的Word等文档中注入恶意脚本。

  • 利用文件名:将XSS攻击代码直接写入文件名,当文件名在页面显示时触发攻击。

🤔 MIME校验为何重要?如何正确进行?

MIME校验的目的是确认上传文件的真实类型,防止攻击者伪装文件。然而,简单的MIME类型检查存在被绕过的风险

一个真实的绕过案例(CVE-2026-32728)

在Parse Server的某些版本中,攻击者可以在Content-Type头后追加参数(例如:Content-Type: image/png;charset=utf-8)。服务器的校验逻辑未能正确处理,导致校验失败,使得本应被拦截的恶意脚本文件(如.html.svg)被成功上传。

正确的MIME校验实践

要有效防御此类绕过,核心在于Content-Type头进行严格的解析和清理

  1. 剥离MIME参数 :在验证文件类型前,必须先去除Content-Type值中;及之后的所有内容,仅保留纯MIME类型(如image/png)进行比对。

  2. 清理Content-Type :在代码中,应先提取并清理Content-Type头,获取干净的MIME类型后再进行后续校验。

🛡️ 如何增加MIME校验?一份实践指南

  1. 服务端校验是必须永远不要信任客户端的校验,所有验证都必须在服务端执行。

  2. 采用"白名单"策略:只允许明确需要的文件扩展名和MIME类型。

  3. "扩展名- MIME类型"配对验证

    • 先验证扩展名:检查文件扩展名是否在白名单中。

    • 再验证MIME类型 :使用库(如Apache Tika、file命令)检测文件真实类型,并与扩展名对应的MIME类型比对。

  4. 清理Content-Type :解析Content-Type时,务必先剥离参数。

    复制代码
    # Python示例
    python 复制代码
    import re
    def get_clean_mime(content_type_header):
        if not content_type_header:
            return None
        # 使用正则或split(';')[0]提取;之前的部分
        clean_mime = content_type_header.split(';')[0].strip()
        return clean_mime
    
    # 使用
    raw_header = "image/png;charset=utf-8"
    mime_type = get_clean_mime(raw_header)  # 结果为 "image/png"

    #nodejs

    javascript 复制代码
    /**
     * 从原始 Content-Type 头中提取纯 MIME 类型(去除 charset 等参数)
     * @param {string} contentTypeHeader - 原始请求头值,如 "image/png;charset=utf-8"
     * @returns {string|null} - 清理后的 MIME 类型(小写),若无效则返回 null
     */
    function getCleanMime(contentTypeHeader) {
      if (!contentTypeHeader || typeof contentTypeHeader !== 'string') {
        return null;
      }
      // 取分号前部分,去除首尾空格,并转为小写
      const clean = contentTypeHeader.split(';')[0].trim().toLowerCase();
      // 可选:检查是否符合基本 MIME 格式(type/subtype)
      if (!/^[a-z0-9\-+]+\/[a-z0-9\-+]+$/.test(clean)) {
        return null; // 非标准格式,拒绝
      }
      return clean;
    }
    
    // 使用示例
    const rawHeader = req.headers['content-type']; // 例如 "image/png;charset=utf-8"
    const mimeType = getCleanMime(rawHeader);
    if (mimeType === 'image/png') {
      // 继续校验文件扩展名、内容等
    }

    #JAVA

    java 复制代码
    import java.util.regex.Pattern;
    
    public class MimeUtils {
        private static final Pattern MIME_PATTERN = 
            Pattern.compile("^[a-z0-9\\-+]+/[a-z0-9\\-+]+$", Pattern.CASE_INSENSITIVE);
    
        /**
         * 从原始 Content-Type 头中提取纯 MIME 类型(去除参数)
         * @param contentTypeHeader 原始头值,如 "image/png;charset=utf-8"
         * @return 清理后的小写 MIME 类型,若无效则返回 null
         */
        public static String getCleanMime(String contentTypeHeader) {
            if (contentTypeHeader == null || contentTypeHeader.isEmpty()) {
                return null;
            }
            // 取分号前部分,去除首尾空格,并转为小写
            String[] parts = contentTypeHeader.split(";");
            String mime = parts[0].trim().toLowerCase();
            // 可选:格式校验
            if (!MIME_PATTERN.matcher(mime).matches()) {
                return null;
            }
            return mime;
        }
    }
    
    // 使用示例(Spring Controller 中)
    String rawHeader = request.getHeader("Content-Type");
    String mimeType = MimeUtils.getCleanMime(rawHeader);
    if ("image/png".equals(mimeType)) {
        // 继续验证扩展名和实际文件内容
    }
    bash 复制代码
    ​
    # -----------------------------------------------------
    # - 🚀 Powered by Moshow郑锴
    # - 🌟 Might the holy code be with you!
    # -----------------------------------------------------
    # 💻 CSDN 👉 https://zhengkai.blog.csdn.net
    # 📂 Github 👉 https://github.com/moshowgame
  5. 文件内容深度检查:对于图片等文件,使用库重绘或解析其结构,确保内容符合规范,剔除隐藏的恶意代码。

  6. 安全地提供文件服务

    • 使用独立域名:将用户上传文件与主应用隔离。

    • 设置正确的Content-Type :服务文件时,由服务器明确指定Content-Type,而非依赖用户上传的值。

    • 添加Content-Disposition :添加Content-Disposition: attachment; filename="..."头,强制浏览器下载而非渲染执行。

  7. 部署内容安全策略(CSP):通过CSP限制页面可执行的脚本来源,即使XSS被注入,也能极大限制其危害。

💎 总结

防御文件存储型XSS,关键在于永远不要信任用户输入 。正确的MIME校验是在服务端,对Content-Type头进行解析和清理后,再结合文件扩展名白名单、文件内容检查等多重验证。同时,通过安全地提供文件服务和部署CSP等措施,构建纵深防御体系。