UNPKG

@stevenleep/sandbox

Version:

A powerful JavaScript sandbox library that provides multiple sandbox implementation options for safely executing untrusted code in browser environments.

382 lines (286 loc) 10.7 kB
# @stevenleep/sandbox A powerful JavaScript sandbox library that provides multiple sandbox implementation options for safely executing untrusted code in browser environments. Perfect for micro-frontends and applications that need to run third-party code securely. ## Features - **Multiple sandbox implementations**: - **ProxySandbox**: Based on Proxy to intercept global object operations - **WithEvalSandbox**: Based on with+eval to execute code, isolating global variables - **ShadowRealmSandbox**: Based on the new ShadowRealm API (with polyfill and fallback options) - **SnapshotSandbox**: Based on snapshots to save and restore global state - **Enhanced Security**: - Complete isolation from global objects - Strict mode execution by default - Prevention of prototype pollution attacks - Code validation to detect potentially dangerous patterns - Blacklisting of sensitive APIs - Control over network access - **Resource Management**: - Execution timeouts to prevent infinite loops - Memory usage monitoring - Performance measurement - Automatic cleanup of resources - **Advanced Features**: - Support for asynchronous code execution (async/await) - ES module loading support - Event listener management to prevent memory leaks - Enhanced error handling with detailed information - Symbol handling, including Symbol.unscopables - Tiered implementation approach with graceful fallback ## Installation ```bash npm install @stevenleep/sandbox ``` ## Basic Usage ```javascript import { createSandbox, SandboxType, getBestSandboxType } from '@stevenleep/sandbox'; // Auto-detect best sandbox implementation for current environment const bestSandboxType = getBestSandboxType(); const sandbox = createSandbox(bestSandboxType, { name: 'my-sandbox' }); // Or explicitly select a sandbox type const proxySandbox = createSandbox(SandboxType.Proxy, { name: 'proxy-sandbox', strictMode: true }); // Activate the sandbox sandbox.activate(); // Execute code in the sandbox const result = sandbox.execScript(` const message = 'Hello from sandbox!'; console.log(message); // Won't affect the global environment window.foo = 'bar'; // Return value message; `); console.log(result); // "Hello from sandbox!" console.log(window.foo); // undefined - the property is contained within the sandbox // When finished sandbox.deactivate(); sandbox.destroy(); // Clean up any resources ``` ## ShadowRealm Implementation The ShadowRealmSandbox provides the most secure isolation and uses a tiered implementation approach: 1. Native ShadowRealm API (if available in the browser) 2. ShadowRealm polyfill (automatically used if native implementation is not available) 3. Iframe-based fallback (as last resort if polyfill fails) ```javascript import { ShadowRealmSandbox, isShadowRealmSupported } from '@stevenleep/sandbox'; // Check if ShadowRealm is supported (either native or via polyfill) console.log(`ShadowRealm is ${isShadowRealmSupported() ? 'supported' : 'not supported'}`); // Create a ShadowRealm sandbox const sandbox = new ShadowRealmSandbox({ name: 'shadowrealm-sandbox', initScript: 'console.log("Sandbox initialized")' }); // The implementation will automatically choose the best available option: // - Native ShadowRealm if supported by the browser // - ShadowRealm polyfill if native is not available // - Iframe fallback as last resort // Execute code - this works the same regardless of which implementation is used sandbox.execScript(` // This code runs in complete isolation const privateData = { secret: 'safe' }; // Only explicitly exported values are accessible outside globalThis.exposedValue = 'exposed'; `); // Access exported values const value = await sandbox.execScriptAsync(`globalThis.exposedValue`); console.log(value); // "exposed" // Load an ES module const module = await sandbox.loadModule('https://example.com/module.js'); ``` ## Proxy Sandbox The ProxySandbox provides a good balance of performance and complete isolation: ```javascript import { ProxySandbox } from '@stevenleep/sandbox'; const sandbox = new ProxySandbox({ name: 'proxy-sandbox', strictMode: true, // IMPORTANT: Always set to true for proper isolation blacklist: ['localStorage', 'sessionStorage'], whitelist: ['console', 'setTimeout'] }); sandbox.activate(); // Variables defined in the sandbox stay in the sandbox sandbox.execScript(` // This will be contained within the sandbox window.testValue = 'sandbox value'; // This logs the sandboxed value console.log(window.testValue); // 'sandbox value' // This will log a warning and not be modified due to blacklist localStorage.setItem('test', 'value'); `); // Variables defined in the sandbox are NOT accessible from outside console.log(window.testValue); // undefined - completely isolated sandbox.destroy(); ``` ## WithEval Sandbox The WithEvalSandbox provides simple global variable isolation: ```javascript import { WithEvalSandbox } from '@stevenleep/sandbox'; const sandbox = new WithEvalSandbox({ name: 'eval-sandbox', globals: { // Custom globals to inject customAPI: { getData: () => 'Custom data' } } }); sandbox.activate(); const result = sandbox.execScript(` // Access custom global const data = customAPI.getData(); // Return the data data; `); console.log(result); // "Custom data" sandbox.destroy(); ``` ## ShadowRealm Sandbox The ShadowRealmSandbox uses the new ShadowRealm API (with polyfill and iframe fallback) to provide complete isolation: ```javascript import { ShadowRealmSandbox } from '@stevenleep/sandbox'; const sandbox = new ShadowRealmSandbox({ name: 'shadowrealm-sandbox', // Force using iframe fallback for compatibility forceIframe: true, // Set execution timeout timeLimit: 1000, // Security configuration security: { preventSensitiveAPIs: true, allowNetwork: false }, // Module options if needed moduleOptions: { baseURL: 'https://example.com/', trustedSources: ['https://trusted-cdn.com/'] } }); sandbox.activate(); // Execute code in complete isolation sandbox.execScript(` // Code runs in completely separate environment const secretData = 'This is isolated'; // Define a function we'll export function calculateResult(input) { return input * 2; } `); // Export a function from main environment to sandbox sandbox.exportFunction('multiplyByThree', (value) => value * 3); // Use the exported function in sandbox const result = sandbox.execScript(` // Use the function exported from main environment const result = multiplyByThree(10); return result; `); console.log(result); // 30 // Import a function from sandbox const calculate = sandbox.importFunction('calculateResult'); console.log(calculate(5)); // 10 // Async execution with timeout const asyncResult = await sandbox.execScriptAsync(` return new Promise(resolve => { setTimeout(() => resolve('Completed'), 100); }); `, 500); sandbox.destroy(); ``` ## Snapshot Sandbox The SnapshotSandbox captures and restores window state: ```javascript import { SnapshotSandbox } from '@stevenleep/sandbox'; const sandbox = new SnapshotSandbox({ name: 'snapshot-sandbox' }); // Save current window state sandbox.activate(); // Run code that modifies window sandbox.execScript(` window.modifiedValue = 'modified'; `); console.log(window.modifiedValue); // "modified" // Restore original window state sandbox.deactivate(); console.log(window.modifiedValue); // undefined - window restored ``` ## Security Considerations - **ShadowRealmSandbox**: Provides the strongest isolation - Native implementation offers true realm-based isolation - Polyfill provides excellent compatibility across browsers - Even the iframe fallback gives strong isolation guarantees - Best choice for executing untrusted code - **ProxySandbox**: Offers a good balance between isolation and compatibility - **IMPORTANT**: Always set `strictMode: true` for complete isolation - With proper configuration, provides complete isolation of window modifications - Sandbox variables remain in the sandbox and do not leak to global scope - Uses an isolated object model separate from the global window - Good choice for semi-trusted code when ShadowRealm is not available - **WithEvalSandbox**: Provides limited isolation - Mainly isolates global variables, not full security boundary - Good for code organization more than security - Not recommended for untrusted code - **SnapshotSandbox**: Focuses on state restoration rather than isolation - Useful for temporarily modifying window state - Not a security boundary for untrusted code - Good for isolation between trusted components - **General Security Tips**: - Always sanitize inputs before passing to any sandbox - Validate outputs returned from sandbox execution - Apply Content Security Policy (CSP) restrictions - Consider using a web worker for additional isolation - Limit execution time with timeouts - Restrict access to sensitive APIs through blacklists ## Ensuring Complete Isolation For maximum security, use the following patterns: ```javascript import { createSandbox, getBestSandboxType } from '@stevenleep/sandbox'; // Always use the best available sandbox type with strictMode enabled const sandbox = createSandbox(getBestSandboxType(), { name: 'secure-sandbox', strictMode: true, // Critical for isolation blacklist: [ // Restrict access to sensitive APIs 'localStorage', 'sessionStorage', 'indexedDB', 'WebSocket', 'XMLHttpRequest', 'fetch', // Prevent DOM access 'document', 'navigator', 'location' ] }); sandbox.activate(); // Variables defined inside stay inside sandbox.execScript(` window.secretData = "This stays in the sandbox"; `); // Global window is completely unmodified console.log(window.secretData); // undefined sandbox.destroy(); ``` ## Advanced Usage ### Performance Measurement ```javascript import { performanceMeasure } from '@stevenleep/sandbox'; const { start, end, measure } = performanceMeasure('code-execution'); start(); // Code to measure const result = heavyComputation(); const duration = end(); console.log(`Execution took ${duration}ms`); ``` ### Error Handling ```javascript import { wrapExecution, formatError } from '@stevenleep/sandbox'; const result = wrapExecution(() => { // Potentially dangerous code return riskyOperation(); }); if (result.error) { console.error('Error occurred:', formatError(result.error)); } else { console.log('Success:', result.value); } ``` ## License MIT