安全用户编程语言相关#

Data: 2026-04-08 00:19:24

我完全明白你的需求!#

你的核心问题是:

想让用户用代码程序化生成方块建筑,但又不能让用户的代码访问到全局变量 k(引擎核心),需要一个安全的沙盒执行环境。


方案对比#

方案体积安全性易用性推荐度
原生 JS 沙盒0kb⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Lua (Fengari)~150kb⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
QuickJS-WASM~300kb+⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
自创 DSL0kb⭐⭐⭐⭐⭐⭐⭐⭐⭐

⭐ 最推荐:原生 JS 沙盒(0kb 额外体积!)#

既然你对体积极度敏感(40kb 引擎),最好的方案是 根本不引入额外库,用浏览器自带的能力做沙盒:

核心思路:Function 构造器 + 白名单注入#

function runUserCode(userCode) {
  // 用户代码只能通过 addBlock 来放方块,返回方块数组
  const blocks = [];

  // 只暴露你允许的 API
  const api = {
    addBlock: (x, y, z, type) => {
      // 限制坐标范围,防止恶意超大数据
      if (Math.abs(x) > 1000 || Math.abs(y) > 1000 || Math.abs(z) > 1000) return;
      if (blocks.length > 50000) return; // 限制方块总数
      blocks.push({ x, y, z, type: type || 1 });
    },
    // 可以给一些数学工具
    sin: Math.sin,
    cos: Math.cos,
    sqrt: Math.sqrt,
    floor: Math.floor,
    random: Math.random,
    PI: Math.PI,
  };

  // 🔑 关键:构造一个与全局完全隔离的函数
  // 参数名就是用户能访问的全部变量
  const paramNames = Object.keys(api);
  const paramValues = Object.values(api);

  try {
    // 创建函数,用户代码在这个函数体内执行
    const sandbox = new Function(
      ...paramNames,
      // 🛡️ 严格模式,禁止 this 泄漏到 window
      `"use strict";\n${userCode}`
    );

    // 调用时,this 设为 null,只传入白名单变量
    sandbox.call(null, ...paramValues);
  } catch (e) {
    console.error('用户代码出错:', e.message);
  }

  return blocks;
}

用户写代码的体验(非常友好):#

// 用户代码示例:生成一个圆柱体
for (let y = 0; y < 20; y++) {
  for (let a = 0; a < PI * 2; a += 0.1) {
    let r = 5;
    addBlock(floor(cos(a) * r), y, floor(sin(a) * r), 2);
  }
}
// 用户代码示例:生成一个球体
let R = 10;
for (let x = -R; x <= R; x++) {
  for (let y = -R; y <= R; y++) {
    for (let z = -R; z <= R; z++) {
      if (sqrt(x*x + y*y + z*z) <= R) {
        addBlock(x, y + R, z, 1);
      }
    }
  }
}

🛡️ 再加一层安全防护#

function runUserCodeSafe(userCode) {
  // 1️⃣ 静态检查:禁止危险关键词
  const banned = [
    'window', 'document', 'globalThis', 'self',
    'eval', 'Function', 'import', 'require',
    'fetch', 'XMLHttpRequest', 'WebSocket',
    'localStorage', 'sessionStorage', 'cookie',
    'setTimeout', 'setInterval', 'requestAnimationFrame',
    '__proto__', 'prototype', 'constructor',
  ];

  // 你的全局变量名!
  banned.push('k');

  for (const word of banned) {
    // 用正则检测,避免字符串中的误判可以更精细
    // 但对于安全场景,宁可误杀
    if (userCode.includes(word)) {
      throw new Error(`禁止使用: "${word}"`);
    }
  }

  // 2️⃣ 限制代码长度
  if (userCode.length > 10000) {
    throw new Error('代码太长了');
  }

  // 3️⃣ 执行(带超时保护)
  const blocks = [];
  let count = 0;
  const MAX_OPS = 1000000; // 防止死循环

  const api = {
    addBlock: (x, y, z, type) => {
      if (++count > MAX_OPS) throw new Error('操作次数超限');
      if (blocks.length > 50000) return;
      if (Math.abs(x) > 500 || Math.abs(y) > 500 || Math.abs(z) > 500) return;
      blocks.push({
        x: Math.floor(x),
        y: Math.floor(y),
        z: Math.floor(z),
        type: (type | 0) || 1
      });
    },
    sin: Math.sin, cos: Math.cos, tan: Math.tan,
    sqrt: Math.sqrt, abs: Math.abs, pow: Math.pow,
    floor: Math.floor, ceil: Math.ceil, round: Math.round,
    random: Math.random, PI: Math.PI, min: Math.min, max: Math.max,
  };

  const paramNames = Object.keys(api);
  const paramValues = Object.values(api);

  try {
    const fn = new Function(...paramNames, `"use strict";\n${userCode}`);
    fn.call(null, ...paramValues);
  } catch (e) {
    console.warn('沙盒执行错误:', e.message);
  }

  return blocks;
}

为什么这个方案最适合你?#

  1. 0 字节额外体积 —— 纯浏览器原生能力
  2. 用户写的就是 JS —— 学习成本为零,for 循环、变量、函数都能用
  3. k 完全不可见 —— 在 new Function 里,用户代码的作用域链上根本没有你的全局变量("use strict" 模式下 this 也是 undefined
  4. 白名单机制 —— 用户只能用你给的 addBlocksincos

Web Worker 方案(依然 0 体积)#

把用户代码丢到 Web Worker 里执行,Worker 天然没有 DOM 访问权限,也完全看不到主线程的任何变量:

function runInWorker(userCode) {
  return new Promise((resolve, reject) => {
    const workerCode = `
      const blocks = [];
      const api = {
        addBlock(x,y,z,t) { 
          if(blocks.length<50000) blocks.push({x:x|0,y:y|0,z:z|0,type:t||1}); 
        },
        sin:Math.sin, cos:Math.cos, sqrt:Math.sqrt,
        floor:Math.floor, PI:Math.PI, random:Math.random,
      };
      
      with(api) {
        try { ${userCode} } catch(e) {}
      }
      postMessage(blocks);
    `;

    const blob = new Blob([workerCode], { type: 'text/javascript' });
    const w = new Worker(URL.createObjectURL(blob));

    // 超时 5 秒自动杀掉(防死循环)
    const timer = setTimeout(() => {
      w.terminate();
      reject(new Error('执行超时'));
    }, 5000);

    w.onmessage = (e) => {
      clearTimeout(timer);
      w.terminate();
      resolve(e.data); // 拿到 blocks 数组
    };

    w.onerror = (e) => {
      clearTimeout(timer);
      w.terminate();
      reject(e);
    };
  });
}

// 使用
const blocks = await runInWorker(userCode);
blocks.forEach(b => k.addBlockToScene(b.x, b.y, b.z, b.type));

Worker 方案的优势:

  • 真正的线程隔离,用户代码 100% 无法访问 kwindowdocument
  • 天然支持超时终止worker.terminate()
  • 死循环也不会卡住主页面
  • ✅ 依然是 0 字节额外体积

总结#

不需要 Lua,不需要任何外部库。new Function + 关键词过滤就够了。如果想要终极安全,用 Web Worker。两个方案都是 0 额外体积,完美匹配你 40kb 引擎的极简哲学。

- end -#

© 2025 –   海牧羽工厂 HMY Factory