随着WebGL、Three.js等技术的普及,越来越多的业务场景开始支持用户上传3D模型文件,比如电商商品展示、在线装修设计、元宇宙社交平台等。然而模型文件本质上是一种结构化的文本或二进制数据,一旦缺乏严格的校验,攻击者就能把恶意内容藏进模型里,这就是所谓的3D模型注入攻击。它的危害并不比传统的SQL注入或XSS小,轻则导致渲染崩溃,重则窃取用户会话、传播恶意代码。

要彻底解决这个问题,核心思路只有两个:过滤和转义。过滤是指在数据进入系统之前,把不合法的内容直接拒绝或剔除;转义是指对确实需要保留的特殊字符做安全化处理,使其失去被解释为代码或指令的能力。下面我们从攻击原理讲起,逐步给出完整的解决方案。
3D模型注入攻击的常见形式和原理
很多人以为模型文件只是一堆顶点坐标和面片索引,实际上现代3D格式的能力远超想象。以GLTF为例,它是基于JSON的结构,除了geometry和material,还包含extensions、extras等自由扩展字段,甚至可以内嵌shader源码和贴图数据。攻击者可以在这些字段中注入JavaScript代码片段,如果前端直接把字段值拼接到DOM中,就会触发存储型XSS。
OBJ格式是纯文本格式,语法宽松,解析器通常会忽略无法识别的行。一些攻击手法会利用解析器的容错特性,在注释符号之后藏入超长字符串、非法数值或畸形索引,试图触发解析器的内存越界或栈溢出,这在C++编写的原生解析库中尤其危险,可能演变为远程代码执行。
还有一种情况是模型引用外部资源。GLTF允许通过URI引用外部的bin数据、贴图甚至脚本类文件,攻击者上传一个指向恶意服务器的模型,前端加载时就会向外部地址发起请求,造成SSRF探测或用户数据外泄。理解这些攻击面,是设计防御方案的前提。
// 一个危险的示例:直接把模型的自定义字段插入页面
gltf.scene.traverse(function (node) {
if (node.userData && node.userData.description) {
// 如果description中包含script标签内容,这里就会执行
document.getElementById('info').innerHTML = node.userData.description;
}
});
上面这段代码就是典型的漏洞写法。模型的userData字段是开放给开发者的任意数据区,攻击者完全可以伪造。正确的做法是使用textContent或者先经过严格的净化处理再插入。
上传环节的过滤:白名单校验与结构检查
防御的第一道防线在服务端。模型上传时,绝不能只看文件扩展名就放行,扩展名是可以随意伪造的。正确做法是结合文件头魔数和结构解析做双重校验:GLTF的JSON版本以glTF标识开头,GLB版本以glTF二进制魔数开头,STL二进制版本有固定的80字节头部加三角形数量结构,这些特征都可以用来确认文件的真实类型。
确认格式合法后,还需要对内容做白名单过滤。所谓白名单,就是只允许出现已知安全的字段和数值类型,其余一律丢弃或拒绝。比如解析GLTF时,检查每个数值是否在合理范围内,顶点坐标是否是有限数字而不是NaN或Infinity,贴图URI是否只指向包内资源而不包含外部域名和协议符号。下面给出一个Node.js端的校验示例。
const ALLOWED_EXTENSIONS = ['gltf', 'glb', 'obj', 'stl'];
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 限制50MB,防止解压炸弹
function validateModelUpload(file) {
const ext = file.name.split('.').pop().toLowerCase();
if (!ALLOWED_EXTENSIONS.includes(ext)) {
throw new Error('不支持的模型格式');
}
if (file.size > MAX_FILE_SIZE) {
throw new Error('文件超出大小限制');
}
// 校验GLTF的JSON结构
if (ext === 'gltf') {
const json = JSON.parse(file.buffer.toString('utf8'));
if (json.asset && json.asset.asset_version !== undefined) {
throw new Error('非法的asset字段');
}
checkUriSafety(json);
}
}
function checkUriSafety(json) {
const walk = (obj) => {
for (const key in obj) {
if (key === 'uri' && typeof obj[key] === 'string') {
// 禁止外部地址和危险协议
if (/^(https?:|data:|file:|\/\/)/i.test(obj[key])) {
throw new Error('模型包含非法外部资源引用');
}
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
walk(obj[key]);
}
}
};
walk(json);
}
除了字段级检查,还应该限制模型的规模上限:三角形数量、贴图分辨率、节点层级深度都设置合理阈值。这不仅是安全考虑,也能防止攻击者上传一个包含上亿面片的模型,让渲染端的GPU和内存被瞬间打满,形成拒绝服务效果。
加载与渲染环节的转义与沙箱隔离
即使上传环节做得很严格,加载环节仍然不能掉以轻心,因为历史上被投毒的模型库、被篡改的CDN资源都真实发生过。加载环节的核心原则是:模型数据只作为数据使用,绝不作为代码执行。具体来说,模型中的自定义字符串如果需要展示到页面,必须经过HTML转义,把尖括号、引号等字符转换为实体形式。
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 展示模型元信息时先转义
const rawName = gltf.scene.userData.displayName || '';
document.getElementById('model-name').textContent = rawName;
// 如果必须用innerHTML,先经过转义
el.innerHTML = '<span>' + escapeHtml(rawName) + '</span>';
对于自定义shader的模型,风险更高,因为shader代码是真正会被GPU执行的程序。强烈建议禁止用户上传的模型携带自定义shader,或者在加载时强制替换为内置的安全shader。如果业务必须支持自定义材质,可以考虑把渲染过程放到Web Worker或独立的iframe沙箱中,即使出问题也不影响主页面。
资源加载也要做隔离。把模型文件存放在独立的资源域名下,与主站业务域名分离,这样即使模型触发了脚本执行,受同源策略限制也无法读取主站的Cookie和本地存储。同时给模型资源的响应头加上Content-Security-Policy和X-Content-Type-Options,进一步压缩攻击空间。
构建持续有效的防护体系
过滤和转义不是一次性的工作,而 should 体系化的长期机制。建议在团队内部建立模型文件的安全基线文档,明确支持的格式版本、字段范围、大小限制,并在CI流程中加入自动化检测:用已知恶意样本测试解析器是否崩溃,用模糊测试工具生成畸形文件验证服务端的健壮性。
监控同样重要。在生产环境中记录模型解析失败、加载超时、渲染异常的日志,一旦某类文件频繁触发异常,及时告警并下架。对于开源解析库,保持版本更新,关注安全公告,很多解析器的内存安全问题都已在后续版本中修复,使用旧版本等于主动留后门。
总结一下,3D模型注入攻击的防御并不神秘,本质上是把Web安全中经过验证的原则迁移到新的数据类型上:上传时做白名单过滤和结构校验,加载时做严格转义和沙箱隔离,运行期配合监控和持续更新。把这三层防线搭好,恶意模型文件就很难找到可乘之机。