diff --git a/packages/agent-script/src/interp/globals/object-array.test.ts b/packages/agent-script/src/interp/globals/object-array.test.ts index 351fa19ff..737bdcd67 100644 --- a/packages/agent-script/src/interp/globals/object-array.test.ts +++ b/packages/agent-script/src/interp/globals/object-array.test.ts @@ -102,6 +102,27 @@ describe("createObjectArrayGlobals", () => { await expect(getClosure(getProperty(globals.Array, "from")).call(["10", globals.Number])).resolves.toEqual([1, 0]); }); + + it("blocks assign and freeze on host prototype objects", async () => { + const globals = createObjectArrayGlobals({ + budget: new Budget() + }); + + expect(() => + getClosure(getProperty(globals.Object, "assign")).call([ + Object.prototype as unknown as SandboxObject, + { + polluted: true + } + ]) + ).toThrowError("Object.assign(target, ...sources) requires an object or array target."); + + expect(await getClosure(getProperty(globals.Object, "freeze")).call([Object.prototype as unknown as SandboxObject])).toBe( + Object.prototype + ); + expect(Object.isFrozen(Object.prototype)).toBe(false); + }); + it("treats sandbox coercion helpers as opaque Object sources", async () => { const globals = createObjectArrayGlobals({ budget: new Budget() diff --git a/packages/agent-script/src/interp/globals/object-array.ts b/packages/agent-script/src/interp/globals/object-array.ts index 7f0787e86..2ca402f87 100644 --- a/packages/agent-script/src/interp/globals/object-array.ts +++ b/packages/agent-script/src/interp/globals/object-array.ts @@ -37,13 +37,7 @@ export function createObjectArrayGlobals(options: { budget: Budget }): ObjectArr name: "fromEntries" }), freeze: createSandboxClosure({ - call: ([value]) => { - if (typeof value === "object" && value !== null) { - Object.freeze(value); - } - - return value; - }, + call: ([value]) => freezeSandboxValue(value), name: "freeze" }), assign: createSandboxClosure({ @@ -80,6 +74,15 @@ export function createObjectArrayGlobals(options: { budget: Budget }): ObjectArr }; } +function freezeSandboxValue(value: SandboxValue): SandboxValue { + if (!isAssignableSandboxTarget(value)) { + return value; + } + + Object.freeze(value); + return value; +} + function assignSandboxValues(target: SandboxValue, sources: readonly SandboxValue[]): SandboxValue { if (target === null || target === undefined) { throw new TypeError("Object.assign(target, ...sources) requires a non-null target."); @@ -105,7 +108,15 @@ function assignSandboxValues(target: SandboxValue, sources: readonly SandboxValu function isAssignableSandboxTarget( value: SandboxValue ): value is SandboxObject & Record | SandboxValue[] & Record { - return typeof value === "object" && value !== null && !isSandboxClosure(value) && !isSandboxPromise(value); + if (typeof value !== "object" || value === null || isSandboxClosure(value) || isSandboxPromise(value)) { + return false; + } + + if (Array.isArray(value)) { + return true; + } + + return Object.getPrototypeOf(value) === Object.prototype; } async function arrayFromSandboxValues(args: readonly SandboxValue[], budget: Budget): Promise {