三维网页应用的安全入口检查
Three.js 场景会引入模型、纹理、着色器和资源分发等新的输入。视觉效果容易占去评审注意力,但这些资源和普通上传文件一样需要明确来源、体积上限和使用方式。
风险并不都来自 WebGL 本身。模型元数据若被当作 HTML 渲染,可能造成 XSS;不受控的着色器可能消耗大量 GPU 时间;客户端泄露的存储凭据会扩大资源访问范围。本文把重点放在输入边界和资源授权,不把渲染故障夸大为已经发生的安全攻击。
1. 3D WebGL 应用的四个入口
三维互动界面的数据源除常规 JSON 接口外,还包括二进制 GLTF/GLB 模型、自定义 GLSL 着色器代码和外部纹理贴图。
1.1 GLTF/GLB 模型结构中的 XSS 载荷注入
GLTF 规范允许模型中包含extras扩展字段以及基于 URI 的外部资源引用。如果应用在加载第三方上传的 3D 模型后,直接提取model.userData或extras中的文本字段并使用dangerouslySetInnerHTML渲染赛博朋克 HUD 悬浮框,攻击者就能通过精心构造的模型文件在用户浏览器中触发跨站脚本攻击(XSS)。
1.2 动态 GLSL Shader 编译漏洞
为了实现赛博朋克 UI 常见的“故障风”(Glitch Effect)或全息扫描线,某些系统允许从服务端拉取用户自定义的 GLSL Shader 片段代码并直接通过new THREE.ShaderMaterial()编译。若未经词法分析与边界隔离,恶意的 Shader 可能引发 GPU 死循环,导致用户显卡驱动崩溃甚至浏览器拒绝服务(DoS)。
1.3 纹理贴图跨域污染与 Canvas 像素越权读取
使用THREE.TextureLoader加载未配置正确 CORS 标头的外部图片时,WebGL 上下文会被标记为“被污染”(Tainted Canvas)。随后任何尝试读取canvas.toDataURL()或readPixels()的操作都会抛出安全安全安全异常。如果防范不当,可能被利用来推导用户的显卡硬件指纹。
1.4 CDN 资源鉴权密钥在前端裸露
昂贵的 3D 资产文件(高模、倾斜摄影点云)需要防止被非法抓包盗用。部分团队在前端 JS 中硬编码了云存储 OSS/S3 的 Master 密钥来动态生成带签名的下载 URL,导致整个资产存储桶遭公开拖库。
2. Three.js 安全渲染与模型过滤代码
下面实现一套完整的生产级 Three.js 3D 资产安全加载与 Shader 校验防护器,屏蔽资产注入与秘钥泄漏隐患。
import * as THREE from "three"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; export interface SafeGLTFOptions { maxVerticesLimit: number; allowedImageDomains: Set<string>; onProgress?: (event: ProgressEvent) => void; } export class CyberpunkSceneSecurityManager { private scene: THREE.Scene; private gltfLoader: GLTFLoader; private allowedDomains: Set<string>; constructor(scene: THREE.Scene, allowedDomains: Set<string>) { this.scene = scene; this.allowedDomains = allowedDomains; this.gltfLoader = new GLTFLoader(); } /** * 安全加载并清洗外部 3D GLTF 模型 */ public async loadAndSanitizeGLTF( gltfUrl: string, options: SafeGLTFOptions ): Promise<THREE.Group> { // 1. 强校验 URL 域名合法性,防止 SSRF 与非法三方源 const parsedUrl = new URL(gltfUrl, window.location.href); if (!this.allowedDomains.has(parsedUrl.hostname) && parsedUrl.protocol !== "blob:") { throw new Error(`[Security Violation] 拒绝从非信任域名加载 3D 资产: ${parsedUrl.hostname}`); } return new Promise((resolve, reject) => { this.gltfLoader.load( gltfUrl, (gltf) => { try { const sanitizedGroup = this.sanitizeModelGroup(gltf.scene, options.maxVerticesLimit); resolve(sanitizedGroup); } catch (err) { reject(err); } }, options.onProgress, (error) => reject(new Error(`GLTF 加载失败: ${error.message}`)) ); }); } /** * 递归遍历 3D Mesh,清除潜在的恶意属性与超出安全阈值的几何体 */ private sanitizeModelGroup(root: THREE.Group, maxVertices: number): THREE.Group { let totalVertices = 0; root.traverse((child) => { // 脱敏 userData 防止 HTML/JS 注入 if (child.userData) { child.userData = this.sanitizeObject(child.userData); } if ((child as THREE.Mesh).isMesh) { const mesh = child as THREE.Mesh; const geometry = mesh.geometry; if (geometry) { const positionAttr = geometry.attributes.position; if (positionAttr) { totalVertices += positionAttr.count; } } // 强行关闭危险的自定义脚本挂载 delete (mesh as any).onBeforeRenderCustom; } }); if (totalVertices > maxVertices) { throw new Error(`[DoS Protection] 3D 模型顶点数 (${totalVertices}) 超出系统上限 (${maxVertices})`); } return root; } /** * 清洗 JSON 属性中的 HTML 标签与 JavaScript 脚本,防止 XSS */ private sanitizeObject(obj: Record<string, any>): Record<string, any> { const sanitized: Record<string, any> = {}; for (const [key, value] of Object.entries(obj)) { if (typeof value === "string") { // 去除所有 <script> 标签与 onXXX 事件属性 sanitized[key] = value .replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "") .replace(/javascript:/gi, "") .replace(/on\w+=/gi, ""); } else if (typeof value === "object" && value !== null) { sanitized[key] = this.sanitizeObject(value); } else { sanitized[key] = value; } } return sanitized; } } /** * 赛博朋克 HUD 赛博风格自定义 GLSL Shader 安全验证器 */ export class SafeCyberpunkShaderMaterial { public static createSafeMaterial(vertexShader: string, fragmentShader: string): THREE.ShaderMaterial { // 校验 GLSL 代码,防止无限循环与语法崩溃 this.validateGLSL(vertexShader); this.validateGLSL(fragmentShader); return new THREE.ShaderMaterial({ vertexShader, fragmentShader, uniforms: { uTime: { value: 0 }, uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, }, transparent: true, depthWrite: false, }); } private static validateGLSL(shaderCode: string): void { // 禁止在 Shader 中使用异常的死循环模式 const dangerousLoopRegex = /for\s*\(\s*;\s*;\s*\)|while\s*\(\s*true\s*\)/gi; if (dangerousLoopRegex.test(shaderCode)) { throw new Error("[GLSL Security Alert] 检测到危险的无限循环 Shader 指令"); } if (shaderCode.length > 50000) { throw new Error("[GLSL Security Alert] Shader 源码体积超出安全限制"); } } }3. 安全获取 CDN 资产的后端 Token 签名接口
为了防止 3D 模型资产被盗用,前端决不能持有存储桶私钥,必须向后端请求短时有效(如 60 秒)的防盗链 URL。
下面展示 Node.js 服务端的签名代理实现:
// server/routes/asset-signer.ts import express, { Request, Response } from "express"; import crypto from "crypto"; const router = express.Router(); const CDN_SECRET_KEY = process.env.CDN_ASSET_SECRET_KEY || "SuperSecretSigningKey"; const CDN_BASE_URL = "https://cdn-assets.yourdomain.com"; router.get("/api/v1/assets/sign-model", (req: Request, res: Response) => { const modelPath = req.query.path as string; const userSession = (req as any).session; if (!userSession || !userSession.userId) { return res.status(401).json({ error: "未授权登录" }); } if (!modelPath || !modelPath.endsWith(".glb")) { return res.status(400).json({ error: "非法的模型路径" }); } // 生成 60 秒后过期的 HMAC HMAC-SHA256 签名 URL const expires = Math.floor(Date.now() / 1000) + 60; const stringToSign = `${modelPath}?expires=${expires}`; const signature = crypto .createHmac("sha256", CDN_SECRET_KEY) .update(stringToSign) .digest("hex"); const signedUrl = `${CDN_BASE_URL}${modelPath}?expires=${expires}&token=${signature}`; return res.json({ signedUrl, expiresInSeconds: 60, }); }); export default router;4. 3D WebGL 视觉与安全防线并重的四项要诀
在追求赛博朋克科技感视觉效果的同时,必须在研发流程中把控好这四条防线:
第一,严密隔离模型中的元数据。在把 GLTF 模型解析出的userData展示到 HUD 界面前,必须进行 DOMPurify 或深度正则转义,切断利用 3D 文件植入 XSS 载荷的通道。
第二,控制 WebGL 场景的渲染算力上限。在安全加载器中监控顶点的总数量(Total Vertices Count)以及贴图分辨率。对单个 3D 模型设置硬性顶点上限,防止超大高模导致用户显卡崩溃。
第三,服务端托管 3D 资产防盗链机制。绝对不在前端保存 S3 或阿里云 OSS 的 AccessKeySecret。所有高价值模型文件的下载,一律走服务端生成的短时 HmacSHA256 签名 URL。
第四,配置严密的 CORS 策略。确保存储 3D 贴图与模型的 CDN 节点只响应信任域名的Access-Control-Allow-Origin请求,防止 WebGL 上下文被恶意三方页面污染。
兼顾惊艳的 3D 视觉体验与坚固的 Web 安全防线,才是现代化 Web 3D 工程落地的正确姿势。