UNPKG

@mariozechner/jailjs

Version:

Lightweight JavaScript interpreter for isolated execution. For plugins, user scripts, and browser extensions. Not for adversarial code - use SandboxJS or isolated-vm for that.

312 lines 11.5 kB
/** * ⚠️ SECURITY TEST SUITE - READ THIS FIRST ⚠️ * * This test suite demonstrates KNOWN vulnerabilities and protections in JailJS. * It is NOT a comprehensive security test suite. * * PURPOSE: * - Show which attacks ARE prevented (constructor escapes, __proto__ access) * - Show which attacks are NOT prevented (prototype pollution) * - Document the security model for users * * WHAT THIS DOES NOT COVER: * - There are MANY undiscovered attack vectors * - New JavaScript features may introduce new escapes * - Timing attacks, side-channels, and other exotic vectors * - Attacks via provided globals (depends on what you pass in) * * SECURITY MODEL: * JailJS provides ISOLATION, not comprehensive sandboxing. * * ✅ PROTECTED AGAINST: * - Constructor chain escapes ([].constructor.constructor) * - __proto__ access * - Direct globalThis/window/self access * * ❌ NOT PROTECTED AGAINST: * - Prototype pollution (if you provide Array/Object) * - Mutation of provided globals * - Resource exhaustion (use maxOps) * - Many other attack vectors * * FOR UNTRUSTED/ADVERSARIAL CODE: * Use SandboxJS (https://github.com/nyariv/SandboxJS) or isolated environments * (Web Workers, separate processes, sandboxed iframes). * * FOR LLM-GENERATED CODE: * Layer JailJS inside sandboxed iframes with minimal API surface. * See LLM-SECURITY.md for detailed guidance. */ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { Interpreter } from "./interpreter"; import { parse } from "./parser"; describe("Security - Sandbox Limitations", () => { let originalArrayPush; beforeEach(() => { // Save original methods before each test originalArrayPush = Array.prototype.push; }); afterEach(() => { // Restore prototypes after each test if (typeof Array.prototype.push !== "function") { Array.prototype.push = originalArrayPush; } delete Array.prototype.HIJACKED; delete Object.prototype.polluted; }); it("VULNERABILITY: Array.prototype pollution affects host environment", () => { const interpreter = new Interpreter({ Array: Array, }, { parse }); // Sandboxed code pollutes Array.prototype interpreter.evaluate(parse(` Array.prototype.HIJACKED = true; `)); // Host code creates a new array - it's affected! const hostArray = []; expect(hostArray.HIJACKED).toBe(true); // Clean up immediately to prevent vitest from breaking delete Array.prototype.HIJACKED; }); it("VULNERABILITY: Array.prototype method can be replaced with string", () => { const interpreter = new Interpreter({ Array: Array, }, { parse }); // Sandboxed code replaces push with a string marker interpreter.evaluate(parse(` Array.prototype.push = "REPLACED"; `)); // Host code sees the replacement const hostArray = []; expect(hostArray.push).toBe("REPLACED"); expect(typeof hostArray.push).toBe("string"); // Restore immediately to prevent vitest from breaking Array.prototype.push = originalArrayPush; }); it("VULNERABILITY: Object.prototype pollution affects all objects", () => { const interpreter = new Interpreter({ Object: Object, }, { parse }); interpreter.evaluate(parse(` Object.prototype.polluted = "compromised"; `)); const hostObj = {}; expect(hostObj.polluted).toBe("compromised"); // Clean up immediately delete Object.prototype.polluted; }); it("VULNERABILITY: Built-in Array is available even if not explicitly provided", () => { const interpreter = new Interpreter({}, { parse }); // Array constructor is available from built-ins const result = interpreter.evaluate(parse(` var arr = []; arr instanceof Array; `)); expect(result).toBe(true); }); it("VULNERABILITY: Providing Array enables prototype pollution", () => { const interpreter = new Interpreter({ Array: Array, }, { parse }); // With Array provided, can pollute it interpreter.evaluate(parse(` Array.prototype.malicious = "pwned"; `)); // Host is affected const hostArray = []; expect(hostArray.malicious).toBe("pwned"); // Clean up delete Array.prototype.malicious; }); describe("PROTECTION: Constructor Chain Escapes", () => { it("should block [].constructor access", () => { const interpreter = new Interpreter({ Array: Array, }, { parse }); const result = interpreter.evaluate(parse(` var arr = []; arr.constructor; `)); expect(result).toBeUndefined(); }); it("should block [].constructor.constructor via array literal", () => { const interpreter = new Interpreter({ Array: Array, }, { parse }); expect(() => { interpreter.evaluate(parse(` var F = [].constructor.constructor; F; `)); }).toThrow(/Cannot read properties of undefined/); }); it("should block constructor via string concatenation", () => { const interpreter = new Interpreter({ Array: Array, }, { parse }); const result = interpreter.evaluate(parse(` var arr = []; var prop = 'const' + 'ructor'; arr[prop]; `)); expect(result).toBeUndefined(); }); it("should block constructor via computed property", () => { const interpreter = new Interpreter({ Array: Array, }, { parse }); const result = interpreter.evaluate(parse(` var arr = []; var key = 'constructor'; arr[key]; `)); expect(result).toBeUndefined(); }); it("should block constructor on objects", () => { const interpreter = new Interpreter({ Object: Object, }, { parse }); const result = interpreter.evaluate(parse(` var obj = {}; obj.constructor; `)); expect(result).toBeUndefined(); }); it("should block constructor on strings", () => { const interpreter = new Interpreter({ String: String, }, { parse }); const result = interpreter.evaluate(parse(` var str = "test"; str.constructor; `)); expect(result).toBeUndefined(); }); }); describe("PROTECTION: __proto__ Access", () => { it("should block __proto__ access", () => { const interpreter = new Interpreter({ Array: Array, }, { parse }); const result = interpreter.evaluate(parse(` var arr = []; arr.__proto__; `)); expect(result).toBeUndefined(); }); it("should block __proto__ via computed property", () => { const interpreter = new Interpreter({ Array: Array, }, { parse }); const result = interpreter.evaluate(parse(` var arr = []; arr['__proto__']; `)); expect(result).toBeUndefined(); }); it("should block __proto__ via string concatenation", () => { const interpreter = new Interpreter({ Array: Array, }, { parse }); const result = interpreter.evaluate(parse(` var arr = []; var key = '__pro' + 'to__'; arr[key]; `)); expect(result).toBeUndefined(); }); }); describe("PROTECTION: Global Object Access", () => { it("should not have globalThis in scope", () => { const interpreter = new Interpreter({}, { parse }); expect(() => { interpreter.evaluate(parse(` globalThis; `)); }).toThrow(/globalThis is not defined/); }); it("should not have window in scope", () => { const interpreter = new Interpreter({}, { parse }); expect(() => { interpreter.evaluate(parse(` window; `)); }).toThrow(/window is not defined/); }); it("should not have self in scope", () => { const interpreter = new Interpreter({}, { parse }); expect(() => { interpreter.evaluate(parse(` self; `)); }).toThrow(/self is not defined/); }); it("should return undefined for 'this' in function context", () => { const interpreter = new Interpreter({}, { parse }); const result = interpreter.evaluate(parse(` var getThis = function() { return this; }; getThis(); `)); expect(result).toBeUndefined(); }); }); describe("PROTECTION: Reflection APIs Not Available", () => { it("should not have Reflect in scope", () => { const interpreter = new Interpreter({}, { parse }); expect(() => { interpreter.evaluate(parse(` Reflect; `)); }).toThrow(/Reflect is not defined/); }); it("should not have Proxy in scope by default", () => { const interpreter = new Interpreter({}, { parse }); expect(() => { interpreter.evaluate(parse(` Proxy; `)); }).toThrow(/Proxy is not defined/); }); }); describe("LIMITATION: Prototype Pollution Still Possible", () => { it("KNOWN ISSUE: Can pollute Array.prototype properties", () => { const interpreter = new Interpreter({ Array: Array, }, { parse }); interpreter.evaluate(parse(` Array.prototype.evil = "still works"; `)); const hostArray = []; expect(hostArray.evil).toBe("still works"); // Clean up delete Array.prototype.evil; }); it("KNOWN ISSUE: Can pollute Object.prototype properties", () => { const interpreter = new Interpreter({ Object: Object, }, { parse }); interpreter.evaluate(parse(` Object.prototype.evil = "still works"; `)); const hostObj = {}; expect(hostObj.evil).toBe("still works"); // Clean up delete Object.prototype.evil; }); it("KNOWN ISSUE: Can replace prototype methods with non-functions", () => { const interpreter = new Interpreter({ Array: Array, }, { parse }); interpreter.evaluate(parse(` Array.prototype.push = "HIJACKED"; `)); const hostArray = []; expect(hostArray.push).toBe("HIJACKED"); // Restore Array.prototype.push = originalArrayPush; }); }); }); //# sourceMappingURL=security.test.js.map