如果把任意的垃圾文件后缀名修改成允许上传的类型,不仅会造成程序崩溃,还有可能会浪费不必要的资源
这种恶意上传的操作,不仅带来安全隐患,还会造成不必要的性能开销
前置知识点
-
Blob 对象
Blob(Binary Large Object)二进制类型的大对象,表示一个不可变、原始数据的类文件对象。可以通过
slice()方法将它们分割成为非常小的数据块File接口基于Blob实现,继承了其功能并将其扩展,使其支持用户系统上的文件 -
FileReader 对象
主要用于将文件内容读入 内存,通过一系列 异步接口 读取本地文件内容并输出结果
readAsArrayBuffer()按字节读取文件内容,结果用 ArrayBuffer 对象表示readAsText()按字节读取文件内容,结果用字符串表示readAsDataURL()读取文件内容,结果用 data:URL 格式的 Base64 字符串表示 -
ArrayBuffer 对象
表示通用的、固定长度的原始二进制数据缓冲区,它是一个
二进制字节数组 -
Uint8Array 数组类型
表示一个
8位无符号整型数组,创建时内容被初始化为0。创建完后,可以以对象的方式或使用数组下标索引的方式引用数组中的元素
前置插件配置
-
安装 VSCode 插件
按下
Ctrl + P激活打开窗口,输入ext install slevesque.vscode-hexdump回车后将会自动安装该插件 -
使用插件查看文件的十六进制内容(以 jpg 文件为例)
常规类型校验
-
使用 HTML 属性
<input type="file" accept=".mp4, .mov"> <input type="file" accept=".png, .jpg, .jpeg, .gif">缺点:可通过浏览器控制台删除
accept属性,绕过限制 -
使用文件 MIME 类型
const validFileType = Object.assign(Object.create(null), { 'video': ['video/mp4', 'video/quicktime'], 'image': ['image/png', 'image/jpeg', 'image/gif'], }) const validator = (type, files) => files.map(file => ({ filename: file.name, valid: validFileType[type].includes(file.type) })).filter(f => !f.valid)缺点:可通过本地修改文件扩展名,绕过限制
安全性类型校验
-
核心思想
使用
slice()方法截取文件头部数据,将其转换为十六进制字符串,然后进行逻辑判断 -
配置文件类型映射策略
const typeMapping = new Map([ ['mp4', { num: 4, hexs: ['00 00 00 18', '00 00 00 20', '00 00 00 1C'] }], ['mov', { num: 8, hexs: ['00 00 00 14 66 74 79 70'] }], ['jpg', { num: 3, hexs: ['FF D8 FF'] }], ['png', { num: 8, hexs: ['89 50 4E 47 0D 0A 1A 0A'] }], ['gif', { num: 4, hexs: ['47 49 46 38'] }], ['pdf', { num: 7, hexs: ['25 50 44 46 2D 31 2E'] }], ['zip', { num: 6, hexs: ['50 4B 03 04 14 00'] }], ['excel', { num: 4, hexs: ['50 4B 03 04'] }], ['image', { num: 8, hexs: ['FF D8 FF', '89 50 4E 47 0D 0A 1A 0A', '47 49 46 38'] }], ['video', { num: 8, hexs: ['00 00 00 18', '00 00 00 20', '00 00 00 1C', '00 00 00 14 66 74 79 70'] }] ])使用
num来配置提取文件头部数据的范围,提取起始处的索引从0开始使用
hexs来配置提取范围对应的十六进制字符串,用于策略逻辑判断 -
根据文件类型读取映射策略
const validFileType = async (type, file) => { const { num, hexs } = typeMapping.get(type) const str = await blobToString(file.slice(0, num)) if (type === 'image' || type === 'video') { return hexs.some(hex => str.startsWith(hex)) } else { return str === hexs.join() } } -
文件的二进制数据转为十六进制字符串
/** * @param {Blob} blob 二进制文件数据 * @return {String} 16进制字符串 */ const blobToString = async blob => { const buffer = await blob.arrayBuffer() return [...new Uint8Array(buffer)].map(n => n.toString(16).toUpperCase().padStart(2, '0')).join(' ') } -
文件类型校验
/** * @param {String} type 文件类型 * @param {Array} files 文件对象 * @return {Array} 文件类型校验不通过的文件列表 */ const validator = async (type, files) => (await Promise.all(files.map(async file => ({ filename: file.name, valid: await validFileType(type, file) })))).filter(f => !f.valid)经测试,即便通过本地修改文件扩展名,文件也会被校验限制
-
总结
很多攻击方式都是把可执行文件的后缀名进行修改,然后上传到服务器进行攻击,可见文件类型校验的重要性
浏览器兼容性
-
在 Safari 浏览器下报错
TypeError: blob.arrayBuffer is not a function. (In 'blob.arrayBuffer()', 'blob.arrayBuffer' is undefined) -
通过 外观模式 解决兼容性
function arrayBuffer(blob) { return new Promise(resolve => { const reader = new FileReader() reader.readAsArrayBuffer(blob) reader.onload = event => resolve(event.target.result) }) } const blobToString = async blob => { let buffer = null try { buffer = await blob.arrayBuffer() } catch { buffer = await arrayBuffer(blob) } return [...new Uint8Array(buffer)].map(n => n.toString(16).toUpperCase().padStart(2, '0')).join(' ') }使用
FileReader的readAsArrayBuffer方法异步读取Blob对象为ArrayBuffer数据对象,在错误捕获中进行兼容处理一起学习,加群交流看 沸点