/** * FileValidator.js * 封装好的文件安全校验类 */ // ========================================== // 1. 预定义:已知文件类型的魔数 (Signature Database) // ========================================== const KNOWN_SIGNATURES = { // === 图片 === png: '89504E470D0A1A0A', jpg: 'FFD8FF', jpeg: 'FFD8FF', gif: '47494638', webp: '52494646', // RIFF Header // === 文档 (Office 新版 - ZIP 格式) === docx: '504B0304', xlsx: '504B0304', pptx: '504B0304', // === 文档 (Office 旧版 - OLECF 格式) === doc: 'D0CF11E0', xls: 'D0CF11E0', ppt: 'D0CF11E0', // === 其他 === pdf: '25504446', // === 纯文本 (无固定魔数,需特殊算法检测) === txt: 'TYPE_TEXT', csv: 'TYPE_TEXT', md: 'TYPE_TEXT', json: 'TYPE_TEXT', }; // ========================================== // 2. 核心类定义 // ========================================== export class FileValidator { /** * 构造函数 * @param {Object} options 配置项 * @param {number} [options.maxSizeMB=10] 最大文件大小 (MB) * @param {string[]} [options.allowedExtensions] 允许的扩展名列表 (如 ['jpg', 'png']),默认允许全部已知类型 */ version = '1.0.0'; constructor(options = {}) { // 配置大小 (默认 10MB) this.maxSizeMB = options.maxSizeMB || 10; // 配置允许的类型 // 如果传入了 allowedExtensions,则只使用传入的;否则使用全部 KNOWN_SIGNATURES if (options.allowedExtensions && Array.isArray(options.allowedExtensions)) { this.allowedConfig = {}; options.allowedExtensions.forEach((ext) => { const key = ext.toLowerCase(); if (KNOWN_SIGNATURES[key]) { this.allowedConfig[key] = KNOWN_SIGNATURES[key]; } else { console.warn(`[FileValidator] 未知的文件类型: .${key},已忽略`); } }); } else { this.allowedConfig = { ...KNOWN_SIGNATURES }; } } /** * 辅助:ArrayBuffer 转 Hex 字符串 */ _bufferToHex(buffer) { return Array.prototype.map .call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2)) .join('') .toUpperCase(); } /** * 辅助:纯文本抽样检测 */ _isCleanText(buffer) { const bytes = new Uint8Array(buffer); const checkLen = Math.min(bytes.length, 1000); let suspiciousCount = 0; for (let i = 0; i < checkLen; i++) { const byte = bytes[i]; // 允许常见控制符: 9(Tab), 10(LF), 13(CR) // 0-31 范围内其他的通常是二进制控制符 if (byte < 32 && ![9, 10, 13].includes(byte)) { suspiciousCount++; } } // 如果可疑字符占比 < 5%,认为是纯文本 return suspiciousCount / checkLen < 0.05; } /** * 执行校验 * @param {File} file 文件对象 * @returns {Promise} */ validate(file) { return new Promise((resolve, reject) => { // 1. 基础对象检查 if (!file || !file.name) return reject('无效的文件对象'); // 2. 大小检查 if (file.size > this.maxSizeMB * 1024 * 1024) { return reject(`文件大小超出限制 (最大 ${this.maxSizeMB}MB)`); } // 3. 后缀名检查 const fileName = file.name.toLowerCase(); const extension = fileName.substring(fileName.lastIndexOf('.') + 1); // 检查是否在配置的白名单中 const expectedMagic = this.allowedConfig[extension]; if (!expectedMagic) { return reject(`不支持的文件格式: .${extension}`); } // 4. 读取二进制头进行魔数校验 const reader = new FileReader(); reader.onload = (e) => { const buffer = e.target.result; let isSafe = false; // 分支处理:纯文本 vs 二进制 if (expectedMagic === 'TYPE_TEXT') { if (this._isCleanText(buffer)) { isSafe = true; } else { return reject(`文件异常:.${extension} 文件包含非法二进制内容`); } } else { // 获取文件头 Hex (读取足够长的字节以覆盖最长的魔数,PNG需8字节) const fileHeader = this._bufferToHex(buffer.slice(0, 8)); // 使用 startsWith 匹配 if (fileHeader.startsWith(expectedMagic)) { isSafe = true; } else { return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`); } } if (isSafe) resolve(true); }; reader.onerror = () => reject('文件读取失败,无法校验'); // 读取前 1KB 进行判断 reader.readAsArrayBuffer(file.slice(0, 1024)); }); } } // 【demo】 // 如果传入了 allowedExtensions,则只使用传入的;否则使用全部 KNOWN_SIGNATURES // const imageValidator = new FileValidator({ // maxSizeMB: 5, // allowedExtensions: ['png', 'jpg', 'jpeg'], // }); // imageValidator // .validate(file) // .then(() => { // statusDiv.textContent = `检测通过: ${file.name}`; // statusDiv.style.color = 'green'; // console.log('图片校验通过,开始上传...'); // // upload(file)... // }) // .catch((err) => { // statusDiv.textContent = `检测失败: ${err}`; // statusDiv.style.color = 'red'; // });