JavaScript沙箱:保护你的前端应用安全

更新日期: 2025-11-14 阅读: 40 标签: 沙箱

在现代前端开发中,我们经常需要运行不确定是否安全的代码。可能是第三方插件,可能是用户提交的脚本,也可能是微前端架构中的子应用。这些时候,JavaScript沙箱就变得特别重要。


什么是JavaScript沙箱?

简单来说,沙箱就是一个隔离的运行环境。它让代码在一个受限制的空间里执行,不会影响到主应用。

想象一下,沙箱就像儿童玩的沙池。孩子可以在里面随意玩耍,但沙子不会弄得到处都是。

沙箱主要解决这些问题:

  • 防止全局变量被意外修改

  • 限制对敏感api的访问

  • 控制代码的资源使用

  • 避免原型链被污染


什么地方需要用到沙箱?

微前端应用
多个前端应用同时运行时,需要互相隔离。

在线代码编辑器
像CodePen、JSFiddle这样的网站,需要安全地执行用户代码。

第三方插件系统
允许第三方开发者为你的应用开发插件。

动态配置系统
用户可以通过输入代码来自定义行为。


基于Proxy的沙箱实现

ES6引入的Proxy特性,让我们能够很好地实现沙箱。

class ProxySandbox {
  constructor() {
    // 记录沙箱运行状态
    this.active = false;
    // 记录新增的全局变量
    this.addedProperties = new Map();
    // 记录修改过的变量原始值
    this.originalValues = new Map();
    // 保存原始window对象
    this.originalWindow = window;
    
    // 创建假的window对象
    this.fakeWindow = Object.create(null);
    
    // 创建代理对象
    this.proxyWindow = new Proxy(this.fakeWindow, {
      set: (target, key, value) => {
        if (this.active) {
          // 如果是新增的属性
          if (!(key in this.originalWindow)) {
            this.addedProperties.set(key, value);
          } 
          // 如果是修改现有属性,且还没记录原始值
          else if (!this.originalValues.has(key)) {
            this.originalValues.set(key, this.originalWindow[key]);
          }
          
          // 只在假window上设置值
          target[key] = value;
        }
        return true;
      },
      
      get: (target, key) => {
        // 先从假window中找
        if (key in target) {
          return target[key];
        }
        
        // 安全地访问原始window的属性
        const value = this.originalWindow[key];
        
        if (this.isSafeProperty(key, value)) {
          // 如果是函数,绑定正确的this
          return typeof value === 'function' ? value.bind(this.originalWindow) : value;
        }
        
        return undefined;
      }
    });
  }
  
  isSafeProperty(key, value) {
    // 定义危险属性列表
    const dangerousProps = [
      'location', 'top', 'parent', 
      'eval', 'Function', 'XMLHttpRequest',
      'fetch', 'localStorage', 'sessionStorage'
    ];
    
    if (dangerousProps.includes(key)) {
      return false;
    }
    
    // 检查函数是否包含危险代码
    if (typeof value === 'function') {
      const funcString = value.toString();
      if (funcString.includes('eval') || funcString.includes('Function')) {
        return false;
      }
    }
    
    return true;
  }
  
  start() {
    this.active = true;
  }
  
  stop() {
    this.active = false;
    
    // 删除新增的属性
    for (const [key] of this.addedProperties) {
      delete this.originalWindow[key];
    }
    
    // 恢复修改的属性
    for (const [key, value] of this.originalValues) {
      this.originalWindow[key] = value;
    }
    
    this.addedProperties.clear();
    this.originalValues.clear();
  }
  
  run(code) {
    this.start();
    try {
      const func = new Function('window', `with(window) { ${code} }`);
      return func(this.proxyWindow);
    } catch (error) {
      console.error('沙箱执行出错:', error);
    } finally {
      this.stop();
    }
  }
}

// 使用例子
const sandbox = new ProxySandbox();
sandbox.run(`
  var myVar = '只在沙箱内有效';
  console.log(myVar); // 正常执行
`);

这个实现的关键点:

  • 用Proxy拦截所有属性访问

  • with语句改变代码的作用域

  • 记录所有修改,便于后续清理


基于iframe的沙箱方案

iframe本身就有环境隔离的能力,是另一种实现沙箱的方式。

class IframeSandbox {
  constructor() {
    this.iframe = null;
    this.isReady = false;
    this.waitingTasks = [];
    this.setup();
  }
  
  setup() {
    this.iframe = document.createElement('iframe');
    this.iframe.style.display = 'none';
    document.body.appendChild(this.iframe);
    
    // 等待iframe加载完成
    this.iframe.onload = () => {
      this.isReady = true;
      this.processWaitingTasks();
    };
    
    // 设置沙箱权限
    this.iframe.sandbox = 'allow-scripts';
    
    this.iframe.srcdoc = `
      <!DOCTYPE html>
      <html>
      <head>
        <meta charset="utf-8">
      </head>
      <body>
        <script>
          window.addEventListener('message', function(event) {
            try {
              const result = eval(event.data.code);
              window.parent.postMessage({
                id: event.data.id,
                result: result,
                success: true
              }, '*');
            } catch (error) {
              window.parent.postMessage({
                id: event.data.id,
                error: error.message,
                success: false
              }, '*');
            }
          });
        </script>
      </body>
      </html>
    `;
    
    // 监听iframe返回的消息
    window.addEventListener('message', this.handleMessage.bind(this));
  }
  
  handleMessage(event) {
    if (event.source !== this.iframe.contentWindow) return;
    
    const { id, result, error, success } = event.data;
    const callback = this.callbacks.get(id);
    
    if (callback) {
      if (success) {
        callback(null, result);
      } else {
        callback(error, null);
      }
      this.callbacks.delete(id);
    }
  }
  
  execute(code, callback) {
    const taskId = Date.now().toString();
    
    if (!this.callbacks) {
      this.callbacks = new Map();
    }
    
    this.callbacks.set(taskId, callback);
    
    const task = {
      id: taskId,
      code: code
    };
    
    if (this.isReady) {
      this.iframe.contentWindow.postMessage(task, '*');
    } else {
      this.waitingTasks.push(task);
    }
  }
  
  processWaitingTasks() {
    while (this.waitingTasks.length > 0) {
      const task = this.waitingTasks.shift();
      this.iframe.contentWindow.postMessage(task, '*');
    }
  }
  
  destroy() {
    if (this.iframe && this.iframe.parentNode) {
      this.iframe.parentNode.removeChild(this.iframe);
    }
  }
}

// 使用例子
const iframeSandbox = new IframeSandbox();
iframeSandbox.execute('2 + 3', (err, result) => {
  console.log('计算结果:', result); // 5
});

iframe沙箱的优势:

  • 真正的环境隔离

  • 安全性很高

  • 浏览器原生支持


在微前端中的应用

微前端架构中,多个应用可能同时运行,沙箱特别重要。

class MicroAppSandbox {
  constructor(appId) {
    this.appId = appId;
    this.windowSnapshot = new Map();
    this.modifiedProps = new Map();
    this.setup();
  }
  
  setup() {
    // 记录初始的window状态
    this.takeSnapshot();
    
    // 代理全局事件
    this.patchEvents();
  }
  
  takeSnapshot() {
    for (const key in window) {
      if (window.hasOwnProperty(key)) {
        this.windowSnapshot.set(key, window[key]);
      }
    }
  }
  
  patchEvents() {
    const originalAddEvent = window.addEventListener;
    const originalRemoveEvent = window.removeEventListener;
    
    this.eventListeners = new Map();
    
    // 重写事件监听方法
    window.addEventListener = (type, listener, options) => {
      if (!this.eventListeners.has(type)) {
        this.eventListeners.set(type, new Set());
      }
      this.eventListeners.get(type).add(listener);
      return originalAddEvent.call(window, type, listener, options);
    };
    
    window.removeEventListener = (type, listener, options) => {
      if (this.eventListeners.has(type)) {
        this.eventListeners.get(type).delete(listener);
      }
      return originalRemoveEvent.call(window, type, listener, options);
    };
    
    // 保存原始方法
    this.originalMethods = {
      addEventListener: originalAddEvent,
      removeEventListener: originalRemoveEvent
    };
  }
  
  start() {
    console.log(`启动 ${this.appId} 的沙箱`);
  }
  
  stop() {
    console.log(`停止 ${this.appId} 的沙箱`);
    
    // 清理新增的属性
    const currentProps = new Set(Object.getOwnPropertyNames(window));
    const originalProps = new Set(this.windowSnapshot.keys());
    
    const newProps = [...currentProps].filter(prop => !originalProps.has(prop));
    
    for (const prop of newProps) {
      try {
        delete window[prop];
      } catch (e) {
        window[prop] = undefined;
      }
    }
    
    // 恢复修改的属性
    for (const [key, value] of this.windowSnapshot) {
      if (window[key] !== value) {
        try {
          window[key] = value;
        } catch (e) {
          console.warn(`无法恢复属性 ${key}:`, e);
        }
      }
    }
    
    // 清理事件监听器
    for (const [type, listeners] of this.eventListeners) {
      for (const listener of listeners) {
        window.removeEventListener(type, listener);
      }
    }
    this.eventListeners.clear();
  }
}


在线代码编辑器的安全方案

对于在线代码编辑器,需要更严格的安全控制。

class CodeEditorSandbox {
  constructor() {
    // 允许使用的API列表
    this.safeAPIs = new Set([
      'console', 'setTimeout', 'setInterval', 
      'clearTimeout', 'clearInterval', 'Math', 
      'Date', 'Array', 'Object', 'String', 
      'Number', 'Boolean', 'JSON'
    ]);
    
    this.timeout = 3000; // 3秒超时
  }
  
  createSafeEnvironment() {
    const safeEnv = Object.create(null);
    
    // 只添加安全的API
    for (const api of this.safeAPIs) {
      if (window[api]) {
        safeEnv[api] = window[api];
      }
    }
    
    // 安全的console
    safeEnv.console = {
      log: (...args) => console.log('[安全模式]', ...args),
      warn: (...args) => console.warn('[安全模式]', ...args),
      error: (...args) => console.error('[安全模式]', ...args)
    };
    
    return safeEnv;
  }
  
  async runCode(code) {
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        reject(new Error('代码执行超时'));
      }, this.timeout);
      
      try {
        const safeEnv = this.createSafeEnvironment();
        
        const wrappedCode = `
          with (safeEnv) {
            return (function() {
              "use strict";
              ${code}
            })();
          }
        `;
        
        const func = new Function('safeEnv', wrappedCode);
        const result = func(safeEnv);
        
        clearTimeout(timer);
        resolve(result);
      } catch (error) {
        clearTimeout(timer);
        reject(error);
      }
    });
  }
}

// 使用例子
const editor = new CodeEditorSandbox();

const userCode = `
  const numbers = [1, 2, 3];
  const squares = numbers.map(x => x * x);
  console.log('平方数:', squares);
  return squares;
`;

editor.runCode(userCode)
  .then(result => console.log('结果:', result))
  .catch(error => console.error('错误:', error));


安全检查和性能优化

安全检查

class SafeSandbox extends ProxySandbox {
  constructor() {
    super();
    this.dangerousPatterns = [
      /eval\(/gi,
      /Function\(/gi,
      /document\./gi,
      /window\./gi,
      /localStorage/gi,
      /fetch\(/gi
    ];
  }
  
  checkCodeSafety(code) {
    for (const pattern of this.dangerousPatterns) {
      if (pattern.test(code)) {
        throw new Error(`检测到危险代码: ${pattern}`);
      }
    }
    return true;
  }
  
  safeRun(code) {
    try {
      this.checkCodeSafety(code);
      return this.run(code);
    } catch (error) {
      console.error('安全警告:', error);
      return null;
    }
  }
}

性能优化

class FastSandbox {
  constructor() {
    this.cache = new Map();
  }
  
  getCachedMethod(name) {
    if (this.cache.has(name)) {
      return this.cache.get(name);
    }
    
    const original = window[name];
    if (typeof original === 'function') {
      const bound = original.bind(window);
      this.cache.set(name, bound);
      return bound;
    }
    
    return undefined;
  }
}


实际应用建议

选择沙箱类型的考虑因素

  • Proxy沙箱:适合大多数场景,平衡了性能和安全性

  • iframe沙箱:需要最高安全性时使用

  • 快照沙箱:微前端场景常用

安全最佳实践

  1. 始终使用白名单而不是黑名单

  2. 设置执行超时限制

  3. 记录所有沙箱操作

  4. 定期更新安全规则

性能提示

  1. 缓存频繁访问的方法

  2. 避免不必要的沙箱初始化

  3. 根据场景选择合适的沙箱类型


总结

JavaScript沙箱是现代前端开发中的重要工具。它帮助我们在运行不确定是否安全的代码时,保护主应用的稳定性。

理解沙箱的工作原理,能够帮助我们在以下场景中做出更好的技术选择:

  • 微前端架构设计

  • 在线代码编辑器开发

  • 第三方插件系统

  • 动态配置功能

记住,没有绝对安全的系统。沙箱只是降低风险的工具之一。在实际项目中,应该结合其他安全措施,共同构建可靠的前端应用。

随着Web技术的发展,沙箱机制也在不断进化。保持学习,及时了解新的安全方案,是每个前端开发者的责任。

本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!

链接: https://fly63.com/article/detial/13174

动手写 js 沙箱

市面上现在流行两种沙箱模式,一种是使用iframe,还有一种是直接在页面上使用new Function + eval进行执行。 殊途同归,主要还是防止一些Hacker们 吃饱了没事干,收别人钱来 Hack 你的网站

说说JS中的沙箱

其实在前端编码中,或多或少都会接触到沙箱,可能天真善良的你没有留意到,又可能,你还并不知道它的真正用途,学会使用沙箱,可以避免潜在的代码注入以及未知的安全问题。

如何实现一个 JS 沙箱?

说到沙箱,我们的脑海中可能会条件反射地联想到上面这个画面并瞬间变得兴致满满,下文将逐步介绍“浏览器世界”的沙箱。在计算机安全中,沙箱(Sandbox)是一种用于隔离正在运行程序的安全机制

你不知道的 JS 沙箱隔离

HTML5 增加了越来越多强大的特性和功能,而在这其中,工作线程(Web Worker)概念的推出让人眼前一亮,但未曾随之激起多大的浪花,并被在其随后工程侧的 Angular、Vue、React 等框架的「革命」浪潮所淹没

比 Eval 和 Iframe 更强的新一代 JavaScript 沙箱!

今天我们来看一个进入 statge3 的新的 JavaScript 提案:ShadowRealm API。领域(realm),这个词比较抽象,其实就代表了一个 JavaScript 独立的运行环境,里面有独立的变量作用域。

【微前端】JS沙箱的基本实现

微前端中,为了保证应用之间js环境(主要是window全局变量)的独立,需要使用JS沙箱来对各应用的执行环境进行隔离。qiankun中使用了两种方案来实现这一隔离,分别是:

面向微前端,谈谈 JavaScript 隔离沙箱机制的古往今来

随着微前端的不断发展、被更多的团队采用,越来越多开始对沙箱这个概念有所了解。 沙箱,即 sandbox,意指一个允许你独立运行程序的虚拟环境,沙箱可以隔离当前执行的环境作用域和外部的其他作用域

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!