魔数与文件类型强校验

#0. 背景

在日常业务开发中,经常会遇到对文件的类型进行校验。文件类型校验可以弱校验,即仅根据文件的后缀名进行类型校验。但是这种校验方式无法识别恶意更改文件后缀名的情况。因此也存在对文件类型进行强校验的方式,即读取文件的十六进制流,根据十六进制编码匹配文件类型魔数(Magic Number)进行判断。

#1. 文件类型魔数枚举类

不同文件类型的十六进制编码开头基本不同,且类型相同的文件其十六进制编码开头相同,这也就是文件类型魔数的来源,可以根据这一特性来进行文件类型的判断。

package com.chiaki.utils;

import org.apache.commons.lang3.StringUtils;

/**
 * 魔数枚举类
 *
 * @author chiaki
 * @date 2022/8/19 10:18
 */
public enum MagicNumberEnum {

    /**
     * JPEG  (jpg)
     */
    JPEG("JPG", "FFD8FF"),

    /**
     * PNG
     */
    PNG("PNG", "89504E47"),

    /**
     * GIF
     */
    GIF("GIF", "47494638"),

    /**
     * TIFF (tif)
     */
    TIFF("TIF", "49492A00"),

    /**
     * Windows bitmap (bmp)
     */
    BMP("BMP", "424D"),

    BMP_16("BMP", "424D228C010000000000"), //16色位图(bmp)

    BMP_24("BMP", "424D8240090000000000"), //24位位图(bmp)

    BMP_256("BMP", "424D8E1B030000000000"), //256色位图(bmp)

    /**
     * CAD  (dwg)
     */
    DWG("DWG", "41433130"),

    /**
     * Adobe photoshop  (psd)
     */
    PSD("PSD", "38425053"),

    /**
     * Rich Text Format  (rtf)
     */
    RTF("RTF", "7B5C727466"),

    /**
     * XML
     */
    XML("XML", "3C3F786D6C"),

    /**
     * HTML (html)
     */
    HTML("HTML", "68746D6C3E"),

    /**
     * Email [thorough only] (eml)
     */
    EML("EML", "44656C69766572792D646174653A"),

    /**
     * Outlook Express (dbx)
     */
    DBX("DBX", "CFAD12FEC5FD746F "),

    /**
     * Outlook (pst)
     */
    PST("PST", "2142444E"),

    /**
     * doc;xls;dot;ppt;xla;ppa;pps;pot;msi;sdw;db
     */
    OLE2("OLE2", "0xD0CF11E0A1B11AE1"),

    /**
     * Microsoft Word/Excel 注意:word 和 excel的文件头一样
     */
    XLS("XLS", "D0CF11E0"),

    /**
     * Microsoft Word/Excel 注意:word 和 excel的文件头一样
     */
    DOC("DOC", "D0CF11E0"),

    /**
     * Microsoft Word/Excel 2007以上版本文件 注意:word 和 excel的文件头一样
     */
    DOCX("DOCX", "504B0304"),

    /**
     * Microsoft Word/Excel 2007以上版本文件 注意:word 和 excel的文件头一样 504B030414000600080000002100
     */
    XLSX("XLSX", "504B0304"),

    /**
     * Microsoft Access (mdb)
     */
    MDB("MDB", "5374616E64617264204A"),

    /**
     * Word Perfect (wpd)
     */
    WPB("WPB", "FF575043"),

    /**
     * Postscript
     */
    EPS("EPS", "252150532D41646F6265"),

    /**
     * Postscript
     */
    PS("PS", "252150532D41646F6265"),

    /**
     * Adobe Acrobat (pdf)
     */
    PDF("PDF", "255044462D312E"),

    /**
     * Quicken (qdf)
     */
    QDF("qdf", "AC9EBD8F"),

    /**
     * QuickBooks Backup (qdb)
     */
    QDB("qbb", "458600000600"),

    /**
     * Windows Password  (pwl)
     */
    PWL("PWL", "E3828596"),

    /**
     * ZIP Archive
     */
    ZIP("", "504B0304"),

    /**
     * RAR Archive
     */
    RAR("", "52617221"),

    /**
     * WAVE (wav)
     */
    WAV("WAV", "57415645"),

    /**
     * AVI
     */
    AVI("AVI", "41564920"),

    /**
     * Real Audio (ram)
     */
    RAM("RAM", "2E7261FD"),

    /**
     * Real Media (rm) rmvb/rm相同
     */
    RM("RM", "2E524D46"),

    /**
     * Real Media (rm) rmvb/rm相同
     */
    RMVB("RMVB", "2E524D46000000120001"),

    /**
     * MPEG (mpg)
     */
    MPG("MPG", "000001BA"),

    /**
     * Quicktime  (mov)
     */
    MOV("MOV", "6D6F6F76"),

    /**
     * Windows Media (asf)
     */
    ASF("ASF", "3026B2758E66CF11"),

    /**
     * ARJ Archive
     */
    ARJ("ARJ", "60EA"),

    /**
     * MIDI (mid)
     */
    MID("MID", "4D546864"),

    /**
     * MP4
     */
    MP4("MP4", "00000020667479706D70"),

    /**
     * MP3
     */
    MP3("MP3", "49443303000000002176"),

    /**
     * FLV
     */
    FLV("FLV", "464C5601050000000900"),

    /**
     * 1F8B0800000000000000
     */
    GZ("GZ", "1F8B08"),

    /**
     * CSS
     */
    CSS("CSS", "48544D4C207B0D0A0942"),

    /**
     * JS
     */
    JS("JS", "696B2E71623D696B2E71"),

    /**
     * Visio
     */
    VSD("VSD", "d0cf11e0a1b11ae10000"),

    /**
     * WPS文字wps、表格et、演示dps都是一样的
     */
    WPS("WPS", "d0cf11e0a1b11ae10000"),

    /**
     * torrent
     */
    TORRENT("TORRENT", "6431303A637265617465"),

    /**
     * JSP Archive
     */
    JSP("JSP", "3C2540207061676520"),

    /**
     * JAVA Archive
     */
    JAVA("JAVA", "7061636B61676520"),

    /**
     * CLASS Archive
     */
    CLASS("CLASS", "CAFEBABE0000002E00"),

    /**
     * JAR Archive
     */
    JAR("JAR", "504B03040A000000"),

    /**
     * MF Archive
     */
    MF("MF", "4D616E69666573742D56"),

    /**
     * EXE Archive
     */
    EXE("EXE", "4D5A9000030000000400"),

    /**
     * ELF Executable
     */
    ELF("ELF", "7F454C4601010100"),

    /**
     * Lotus 123 v1
     */
    WK1("WK1", "2000604060"),

    /**
     * Lotus 123 v3
     */
    WK3("WK3", "00001A0000100400"),

    /**
     * Lotus 123 v5
     */
    WK4("WK4", "00001A0002100400"),

    /**
     * Lotus WordPro v9
     */
    LWP("LWP", "576F726450726F"),

    /**
     * Sage(sly.or.srt.or.slt;sly;srt;slt)
     */
    SLY("SLY", "53520100"),

    /**
     * 不存在
     */
    NOT_EXITS_ENUM("", "");

    private final String type;

    private final String num;

    MagicNumberEnum(String type, String num) {
        this.type = type;
        this.num = num;
    }

    public String getNum() {
        return num;
    }

    public String getType() {
        return type;
    }

    /**
     * 根据类型获取魔数类型
     *
     * @param type 类型
     * @return 结果
     */
    public static MagicNumberEnum of(String type) {
        for (MagicNumberEnum anEnum : values()) {
            if (anEnum.getType().equalsIgnoreCase(type)) {
                return anEnum;
            }
        }
        return MagicNumberEnum.NOT_EXITS_ENUM;
    }

    /**
     * 根据十六进制流获取魔数类型
     *
     * @param hex 十六进制流
     * @return 结果
     */
    public static MagicNumberEnum byHex(String hex) {
        if (StringUtils.isBlank(hex)) {
            return MagicNumberEnum.NOT_EXITS_ENUM;
        }
        for (MagicNumberEnum anEnum : values()) {
            if (hex.toUpperCase().startsWith(anEnum.getNum())) {
                return anEnum;
            }
        }
        return MagicNumberEnum.NOT_EXITS_ENUM;
    }
}

#2. 文件工具类

再提供一个文件工具类可以将本地文件或者在线的url文件转换为字节流。

package com.chiaki.utils;

import org.apache.commons.io.IOUtils;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.net.URL;
import java.nio.file.Files;

/**
 * 文件工具类
 *
 * @author chiaki
 * @date 2022/8/2 18:02
 */
public class FileUtil {

    /**
     * 通过网络URL获得文件二进制流
     *
     * @param fileUrl 链接
     * @return 字节数组
     */
    public static byte[] getUrlInputStream(String fileUrl) {
        try (BufferedInputStream in = new BufferedInputStream(new URL(fileUrl).openStream());
             ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            IOUtils.copy(in, out);
            return out.toByteArray();
        } catch (Throwable ignored) {

        }
        return null;
    }

    /**
     * 从文件获取二进制流
     *
     * @param file 文件
     * @return 结果
     */
    public static byte[] getFileBytes(File file) {
        try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()));
             ByteArrayOutputStream out = new ByteArrayOutputStream((int) file.length())) {
            IOUtils.copy(in, out);
            return out.toByteArray();
        } catch (Throwable ignored) {
        }
        return null;
    }
}

#3. 十六进制工具类

提供一个将字节流转换为十六进制编码的工具类。

package com.chiaki.utils;

/**
 * 十六进制转换工具
 *
 * @author chiaki
 * @date 2022/8/2 22:20
 */
public class HexUtil {

    /**
     * 字节数组转十六进制字符串
     *
     * @param bytes 字节数组
     * @return 十六进制字符串
     */
    public static String bytes2HexString(byte[] bytes) {
        StringBuffer sb = new StringBuffer();
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        for (byte aByte : bytes) {
            int v = aByte & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                sb.append(0);
            }
            sb.append(hv);
        }
        return sb.toString();
    }
}

#4. 测试DEMO

写一个测试方法验证下效果。

public static void main(String[] args) {
    File file = new File("/xxx/测试PDF文件.pdf");
    byte[] fileBytes = FileUtil.getFileBytes(file);
    String hex = HexUtil.bytes2HexString(fileBytes);
    MagicNumberEnum magicNumberEnum = MagicNumberEnum.byHex(hex);
    System.out.println(magicNumberEnum.getType());
}