diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ac46ad..db47ce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +### Bug Fixes + +- **logic-engine:** prevent trigger sandbox evaluation from freezing host prototypes while preserving `return`-based logic scripts; reported in [#496](https://github.com/the-open-engine/zeroshot/pull/496). + # [5.3.0](https://github.com/covibes/zeroshot/compare/v5.2.1...v5.3.0) (2026-01-12) ### Bug Fixes diff --git a/src/logic-engine.js b/src/logic-engine.js index b9a108b..d1e639d 100644 --- a/src/logic-engine.js +++ b/src/logic-engine.js @@ -82,10 +82,6 @@ function buildAgentContext(agent) { }; } -function getSafeBuiltins() { - return { Set, Map, Array, Object, String, Number, Boolean, Math, Date, JSON }; -} - function getQuietConsole() { return { log: () => {}, @@ -114,33 +110,19 @@ class LogicEngine { // Build sandbox context const context = this._buildContext(agent, message); - // Create isolated context with frozen prototypes - // This prevents prototype pollution attacks - const isolatedContext = {}; - - // Freeze Object, Array, Function prototypes in the sandbox - isolatedContext.Object = Object.freeze({ ...Object }); - isolatedContext.Array = Array; - isolatedContext.Function = Function; + // Contextify before execution so scripts use VM-owned globals instead of + // host constructors. Keep the function-body contract: trigger scripts use + // return statements throughout built-in templates and user configs. + const sandbox = { ...context }; + vm.createContext(sandbox); - // Copy safe context properties - Object.assign(isolatedContext, context); - - // Wrap script to prevent prototype access const wrappedScript = `(function() { 'use strict'; - // Prevent prototype pollution - const frozenObject = Object; - const frozenArray = Array; - Object.freeze(frozenObject.prototype); - Object.freeze(frozenArray.prototype); - ${script} })()`; - // Create and run in context - vm.createContext(isolatedContext); - const result = vm.runInContext(wrappedScript, isolatedContext, { + // Run in context + const result = vm.runInContext(wrappedScript, sandbox, { timeout: this.timeout, displayErrors: true, }); @@ -168,7 +150,6 @@ class LogicEngine { ledger: ledgerAPI, cluster: buildClusterAPI(this.cluster, clusterId), helpers: buildHelpers(ledgerAPI), - ...getSafeBuiltins(), console: getQuietConsole(), }; } diff --git a/tests/integration/trigger-evaluation.test.js b/tests/integration/trigger-evaluation.test.js index 36ecd22..6543e2e 100644 --- a/tests/integration/trigger-evaluation.test.js +++ b/tests/integration/trigger-evaluation.test.js @@ -6,6 +6,7 @@ */ const assert = require('node:assert'); +const { spawnSync } = require('child_process'); const path = require('path'); const fs = require('fs'); const os = require('os'); @@ -417,6 +418,42 @@ function defineScriptValidationTests() { assert.strictEqual(validResult.valid, true); assert.strictEqual(invalidResult.valid, false); }); + + it('should not freeze host prototypes when evaluating scripts', () => { + const childScript = ` + const LogicEngine = require(${JSON.stringify(path.resolve(__dirname, '../../src/logic-engine'))}); + const messageBus = { + query: () => [], + findLast: () => null, + count: () => 0, + since: () => [], + }; + const engine = new LogicEngine(messageBus, { id: 'test-cluster', agents: [] }); + const result = engine.evaluate( + 'return true;', + { id: 'evaluator', cluster_id: 'test-cluster' }, + { topic: 'TRIGGER' } + ); + console.log(JSON.stringify({ + result, + objectPrototypeFrozen: Object.isFrozen(Object.prototype), + arrayPrototypeFrozen: Object.isFrozen(Array.prototype), + })); + `; + + const child = spawnSync(process.execPath, ['-e', childScript], { + encoding: 'utf8', + }); + + assert.strictEqual(child.status, 0, child.stderr || child.stdout); + const output = JSON.parse(child.stdout.trim()); + + assert.deepStrictEqual(output, { + result: true, + objectPrototypeFrozen: false, + arrayPrototypeFrozen: false, + }); + }); }); }