XSS 跨站脚本攻击预防(文件上传)

XSS 跨站脚本攻击预防(文件上传)

注意:可以根据需求自定义,改造为拦截器、或者 AOP 等方式实现

package com.atguigu.springcloud.test.test;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.extra.spring.SpringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
import org.springframework.web.multipart.MultipartFile;

import java.io.BufferedReader;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;


public class FileUploadUtils {

    private static final Logger log = LoggerFactory.getLogger(FileUploadUtils.class);


    private static final MultipartProperties multipartProperties = SpringUtil.getBean(MultipartProperties.class);


    // 文件类型常量
    public static final String JPEG = "jpeg";
    public static final String JPG = "jpg";
    public static final String PNG = "png";
    public static final String GIF = "gif";
    public static final String PDF = "pdf";
    public static final String ZIP = "zip";
    public static final String RAR = "rar";
    public static final String DOC = "doc";
    public static final String DOCX = "docx";
    public static final String XLS = "xls";
    public static final String XLSX = "xlsx";
    public static final String PPT = "ppt";
    public static final String PPTX = "pptx";

    // 魔数常量
    public static final String JPEG_MAGIC = "FFD8FF";
    public static final String JPG_MAGIC = "FFD8FF";
    public static final String PNG_MAGIC = "89504E47";
    public static final String GIF_MAGIC = "47494638";
    public static final String PDF_MAGIC = "25504446";
    public static final String ZIP_MAGIC = "504B0304";
    public static final String RAR_MAGIC = "52617221";
    public static final String DOC_MAGIC = "D0CF11E0";
    public static final String DOCX_MAGIC = "504B0304";
    public static final String XLS_MAGIC = "D0CF11E0";
    public static final String XLSX_MAGIC = "504B0304";
    public static final String PPT_MAGIC = "D0CF11E0";
    public static final String PPTX_MAGIC = "504B0304";

    // 允许的文件类型
    // key-value : 文件类型-文件魔数
    private static final Map<String, String> FILE_TYPE_MAGIC_NUMBERS = new HashMap<>();

    static {
        FILE_TYPE_MAGIC_NUMBERS.put(JPEG, JPEG_MAGIC);
        FILE_TYPE_MAGIC_NUMBERS.put(JPG, JPG_MAGIC);
        FILE_TYPE_MAGIC_NUMBERS.put(PNG, PNG_MAGIC);
        FILE_TYPE_MAGIC_NUMBERS.put(GIF, GIF_MAGIC);
        FILE_TYPE_MAGIC_NUMBERS.put(PDF, PDF_MAGIC);
        FILE_TYPE_MAGIC_NUMBERS.put(ZIP, ZIP_MAGIC);
        FILE_TYPE_MAGIC_NUMBERS.put(RAR, RAR_MAGIC);
        FILE_TYPE_MAGIC_NUMBERS.put(DOC, DOC_MAGIC);
        FILE_TYPE_MAGIC_NUMBERS.put(DOCX, DOCX_MAGIC);
        FILE_TYPE_MAGIC_NUMBERS.put(XLS, XLS_MAGIC);
        FILE_TYPE_MAGIC_NUMBERS.put(XLSX, XLSX_MAGIC);
        FILE_TYPE_MAGIC_NUMBERS.put(PPT, PPT_MAGIC);
        FILE_TYPE_MAGIC_NUMBERS.put(PPTX, PPTX_MAGIC);
    }

    // 定义更加全面的XSS攻击模式
    private static final Pattern[] XSS_PATTERNS = new Pattern[]{
            // 匹配script标签
            Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
            Pattern.compile("<script(.*?)>(.*?)</script>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
            Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

            // 匹配img、iframe、embed、object标签中的恶意代码
            Pattern.compile("<img(.*?)src[\r\n]*=[\r\n]*\\'(.*?)\\'(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("<img(.*?)src[\r\n]*=[\r\n]*\\\"(.*?)\\\"(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("<iframe(.*?)src[\r\n]*=[\r\n]*\\'(.*?)\\'(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("<iframe(.*?)src[\r\n]*=[\r\n]*\\\"(.*?)\\\"(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("<embed(.*?)src[\r\n]*=[\r\n]*\\'(.*?)\\'(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("<embed(.*?)src[\r\n]*=[\r\n]*\\\"(.*?)\\\"(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("<object(.*?)data[\r\n]*=[\r\n]*\\'(.*?)\\'(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("<object(.*?)data[\r\n]*=[\r\n]*\\\"(.*?)\\\"(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

            // 匹配JavaScript事件处理程序
            Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("onerror(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("onmouseover(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("onclick(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("onfocus(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("onblur(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("onchange(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("onsubmit(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("onreset(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("onselect(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("onunload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("onkeydown(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("onkeyup(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("onkeypress(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

            // 匹配其他危险的JavaScript代码
            Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
            Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),
            Pattern.compile("data:text/html", Pattern.CASE_INSENSITIVE),
            Pattern.compile("document.cookie", Pattern.CASE_INSENSITIVE),
            Pattern.compile("document.write", Pattern.CASE_INSENSITIVE),
            Pattern.compile("window.location", Pattern.CASE_INSENSITIVE),
            Pattern.compile("window.open", Pattern.CASE_INSENSITIVE),
            Pattern.compile("innerHTML", Pattern.CASE_INSENSITIVE),
            Pattern.compile("alert\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("prompt\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("confirm\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

            // 匹配各种形式的javascript关键字
            Pattern.compile("/javascript", Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("/JavaScript", Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("/jscript", Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("/vbscript", Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("/ecmascript", Pattern.MULTILINE | Pattern.DOTALL),

            // 匹配 CSS 表达式
            Pattern.compile("style=(.*?)/\\*<style>\\*/", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("style=(.*?)expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("style=(.*?)behaviour\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("style=(.*?)javascript:(.*?)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

            // 匹配 HTML 属性中的 JavaScript
            Pattern.compile("href[\r\n]*=[\r\n]*\\\"(javascript:(.*?))\\\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("href[\r\n]*=[\r\n]*\\'(javascript:(.*?))\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("src[\r\n]*=[\r\n]*\\\"(javascript:(.*?))\\\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("src[\r\n]*=[\r\n]*\\'(javascript:(.*?))\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

            // 匹配形式如 <a οnlοad=evil() /> 的 XSS
            Pattern.compile("<(.*?)on(load|error|mouseover|click|focus|blur|change|submit|reset|select|unload|keydown|keyup|keypress)=(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

            // 匹配 base64 数据 URI
            Pattern.compile("data:text/html;base64,", Pattern.CASE_INSENSITIVE),

            // 匹配可能的路径级 XSS
            Pattern.compile("/[a-zA-Z0-9\\-_]*[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]/"),
            Pattern.compile("/[a-zA-Z0-9\\-_]*[vV][bB][sS][cC][rR][iI][pP][tT]/"),
            Pattern.compile("/[a-zA-Z0-9\\-_]*[eE][cC][mM][aA][sS][cC][rR][iI][pP][tT]/"),

            // 匹配 XSS 关键字在任何位置的情况
            Pattern.compile("[aA][lL][eE][rR][tT]\\("),
            Pattern.compile("[pP][rR][oO][mM][pP][tT]\\("),
            Pattern.compile("[cC][oO][nN][fF][iI][rR][mM]\\("),
            Pattern.compile("[eE][vV][aA][lL]\\("),
            Pattern.compile("[eE][xX][pP][rR][eE][sS][sS][iI][oO][nN]\\("),

            // 匹配 HTML 的属性值内嵌的 Javascript
            Pattern.compile("value[\r\n]*=[\r\n]*\\\"(.*?)\\\"[\r\n]*onchange[\r\n]*=[\r\n]*\\\"(.*?)\\\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("value[\r\n]*=[\r\n]*\\'(.*?)\\'[\r\n]*onchange[\r\n]*=[\r\n]*\\'(.*?)\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

            // 匹配十六进制编码的script
            Pattern.compile("&#x[0-9a-fA-F]+;"),

            // 匹配HTML实体编码的script
            Pattern.compile("&lt;script&gt;(.*?)&lt;/script&gt;", Pattern.CASE_INSENSITIVE),
            Pattern.compile("&lt;script(.*?)&gt;(.*?)&lt;/script&gt;", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

            // 其他可能的编码方式
            Pattern.compile("src[\r\n]*=[\r\n]*\\\\x22(.*?)\\\\x22", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("src[\r\n]*=[\r\n]*\\\\x27(.*?)\\\\x27", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),

            // 匹配 SVG 相关的注入
            Pattern.compile("<svg(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("<animate(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("<set(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
    };


    /**
     * 获取随机uuid文件名
     *
     * @param originalFileName 原始文件名
     * @param isSimple         是否是简单随机uuid,原生uuid带 "-" 符号
     * @return fileName
     */
    public static String getUuidFileName(String originalFileName, Boolean isSimple) {
        String fileExtension = getFileExtension(originalFileName);
        UUID uuid = UUID.randomUUID(true);
        return uuid.toString(isSimple) + "." + fileExtension;
    }


    /**
     * @param file             被校验文件
     * @param allowFileMaxSize 允许的文件大小,单位为字节
     * @param fileTypes        支持的文件类型
     * @return bool
     */
    public static void isValidFile(MultipartFile file, Long allowFileMaxSize, String... fileTypes) {

        // 检查文件大小
        if (!isValidFileSize(file, allowFileMaxSize)) {
            throw new RuntimeException("上传文件大小超过限制: " + allowFileMaxSize);
        }

        // 检查文件类型
        String originalFilename = file.getOriginalFilename();
        String fileExt = getFileExtension(originalFilename);
        if (!isValidFileType(fileExt, fileTypes)) {
            log.error("暂不支持文件类型: {}", fileExt);
            throw new RuntimeException("暂不支持文件类型: " + fileExt);
        }

        try (InputStream inputStream = file.getInputStream()) {
            // 魔数校验
            if (!isValidFileMagic(inputStream, fileTypes)) {
                throw new RuntimeException("文件内容和文件类型不匹配");
            }

            // Xss校验
            if (containsXSS(inputStream)) {
                throw new RuntimeException("文件包含非法字符");
            }
            log.info("上传文件校验成功");
        } catch (Exception e) {
            log.error("上传文件失败: {}", e.getMessage());
            throw new RuntimeException(e);
        }

    }


    /**
     * @param fileSuffix 文件后缀
     * @param fileTypes  支持的文件类型
     * @return
     */
    private static boolean isValidFileType(String fileSuffix, String[] fileTypes) {
        boolean flag = false;
        if (fileTypes == null || fileTypes.length == 0) {
            flag = FILE_TYPE_MAGIC_NUMBERS.containsKey(fileSuffix.toLowerCase());
        } else {
            for (String fileType : fileTypes) {
                if (fileSuffix.equals(fileType)) {
                    flag = true;
                }
            }
        }
        return flag;
    }

    /**
     * 校验文件大小
     *
     * @param file             文件
     * @param allowFileMaxSize 允许的文件大小
     * @return
     */
    private static boolean isValidFileSize(MultipartFile file, Long allowFileMaxSize) {
        if (allowFileMaxSize == null) {
            allowFileMaxSize = multipartProperties.getMaxFileSize().toBytes();
        }
        log.info("上传文件大小为: {}", file.getSize());
        return file.getSize() <= allowFileMaxSize;
    }

    /**
     * 文件魔数校验
     *
     * @param fis       文件
     * @param fileTypes
     * @return
     */
    private static boolean isValidFileMagic(InputStream fis, String[] fileTypes) throws Exception {

        byte[] bytes = new byte[4];
        fis.read(bytes, 0, bytes.length);
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X", b));
        }
        String magicNumber = sb.toString();
        log.info("上传文件的魔数为: {}", magicNumber);
        if (fileTypes == null || fileTypes.length == 0) {
            for (String magic : FILE_TYPE_MAGIC_NUMBERS.values()) {
                if (magicNumber.startsWith(magic)) {
                    return true;
                }
            }
        } else {
            for (String fileType : fileTypes) {
                String magic = FILE_TYPE_MAGIC_NUMBERS.get(fileType);
                if (magicNumber.startsWith(magic)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 返回文件后缀
     *
     * @param originalFilename 文件名
     * @return
     */
    private static String getFileExtension(String originalFilename) {
        String suffix = "";
        if (originalFilename != null) {
            int lastIndex = originalFilename.lastIndexOf('.');
            if (lastIndex > 0) {
                suffix = originalFilename.substring(lastIndex + 1);
            }
        }
        log.info("上传的文件后缀为: {}", suffix);
        return suffix;
    }


    public static boolean containsXSS(InputStream fis) {
        try (BufferedReader reader = IoUtil.getReader(fis, StandardCharsets.UTF_8)) {
            String currentLine = "";
            while ((currentLine = reader.readLine()) != null) {
                for (Pattern pattern : XSS_PATTERNS) {
                    if (pattern.matcher(currentLine).find()) {
                        return true;
                    }
                }
            }

        } catch (Exception e) {
            log.error("上传文件-流读取操作异常: {}", e.getMessage());
            return false;
        }
        return false;
    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陌守

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值