diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 936d7386c11..4c4ac044ed6 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -47,7 +47,7 @@ ultimate source of truth. - [x] `const`, `let`, and accepted `var` declarations. - [x] Object and array destructuring in declarations, parameters, assignment expressions, and `for...of` bindings. - [x] Nested patterns, defaults, elisions, and rest elements. -- [x] Assignment to identifiers, unblocked plain-object fields, non-negative integer array indexes, and writable URL +- [x] Assignment to identifiers, plain-object fields, non-negative integer array indexes, and writable URL fields. - [x] Direct function declarations are hoisted in program and block statement lists. - [x] Parameter defaults observe a temporal dead zone for later parameters. @@ -213,10 +213,10 @@ ultimate source of truth. synchronous iterator support for `fromEntries`. - [x] `Object.keys` over arrays and tool references. - [x] Object identity is preserved by in-CodeMode Object helpers. -- [x] Prototype traversal and mutation through `__proto__`, `constructor`, and `prototype` are blocked. +- [x] `__proto__`, `constructor`, and `prototype` are ordinary own data keys. Prototype machinery is not observable: + data objects have no prototype, so `({}).constructor` and `[].__proto__` read as `undefined` and `o.__proto__ = x` + sets an own field. - [x] Circular references are rejected when created (`o.self = o`, `array.push(array)`), not at serialization as in JS. -- [ ] Legal own data fields named `__proto__`, `constructor`, or `prototype` are rejected at JSON/tool boundaries and - cannot be created, read, or written in CodeMode; tool path segments with those names remain supported. - [x] `Object.is` for supported data values. - [x] `Object.groupBy` over finite collections and custom synchronous iterators/generators, with string-key coercion and null-prototype results. @@ -280,23 +280,20 @@ ultimate source of truth. use their epoch time) and reject opaque runtime references as data errors. - [x] Unknown static members on global namespaces and on `Number`/`String`/the coercion functions read as `undefined` for feature detection. Calling any undefined value reports a native-style `TypeError` naming the callee, for - example `Math.sum is not a function.` Blocked members (`constructor`, `__proto__`, ...) still throw, - and unknown `Promise` statics keep their descriptive error. + example `Math.sum is not a function.` Unknown `Promise` statics keep their descriptive error. - [x] `Math.sumPrecise` over finite collections and custom synchronous iterators/generators, rejecting non-number elements without coercion. - [x] Global coercing `isFinite` and `isNaN`; opaque runtime references reject as data errors, like `Number(...)`. ## JSON and console -- [x] `JSON.parse` and `JSON.stringify` for supported data objects; the blocked data-key gap listed above still applies. +- [x] `JSON.parse` and `JSON.stringify` for supported data objects. - [x] Numeric/string indentation for `JSON.stringify`. - [x] `JSON.parse` reviver callbacks, including postorder traversal, deletion through `undefined`, and root replacement. Revivers receive `(key, value)` but no `this` holder because CodeMode functions intentionally have no `this`. - [x] `JSON.stringify` function and array replacers. Function replacers receive `(key, value)` in preorder, including the root, but no `this` holder. Array replacers preserve requested property order, deduplicate names, coerce number primitives, and ignore non-string/non-number entries. Primitive wrapper entries remain unsupported. -- [x] JSON callbacks retain the blocked-key boundary: parsed or stringified data containing `__proto__`, `constructor`, - or `prototype` is rejected before callback traversal. - [x] Captured `console.log`, `console.info`, `console.debug`, `console.warn`, and `console.error`. - [x] Captured `console.dir` and `console.table`. @@ -327,7 +324,7 @@ ultimate source of truth. - [x] `test`, `exec`, and `toString`. - [x] Readable `source`, `flags`, `lastIndex`, `hasIndices`, `global`, `ignoreCase`, `multiline`, `sticky`, `unicode`, `unicodeSets`, and `dotAll`. -- [x] Captures, safe named groups (blocked member names are omitted), match `.index`, and stateful global matching. +- [x] Captures, named groups, match `.index`, and stateful global matching. - [x] Integration with supported String methods, including function replacers. - [x] Writable `lastIndex`. - [x] Match `indices` metadata for the `d` flag, including named groups on `exec`, `match`, and `matchAll` results. diff --git a/packages/codemode/src/data.ts b/packages/codemode/src/data.ts index 6eeaa520fca..4eb98f25d34 100644 --- a/packages/codemode/src/data.ts +++ b/packages/codemode/src/data.ts @@ -22,10 +22,6 @@ export class ToolRuntimeError extends Error { } } -const blockedMemberNames = new Set(["__proto__", "constructor", "prototype"]) - -export const isBlockedMember = (name: string): boolean => blockedMemberNames.has(name) - /** * Brings a host-produced runtime value into the program: runtime values pass through, their host * counterparts (Date, RegExp, Map, Set, URL, URLSearchParams) are wrapped, and objects become @@ -118,10 +114,7 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se if (mode === "program") { for (const [key, item] of Object.entries(value)) { if (Object.hasOwn(copied, key)) continue - if (isBlockedMember(key)) { - throw new ToolRuntimeError("InvalidDataValue", `${label} contains blocked property '${key}'.`) - } - Reflect.set(copied, key, copy(item, label, mode, depth + 1, seen)) + define(copied, key, copy(item, label, mode, depth + 1, seen)) } } seen.delete(value) @@ -135,13 +128,16 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se const copied: SafeObject = plain ? (Object.create(null) as SafeObject) : {} for (const [key, item] of Object.entries(value)) { - if (isBlockedMember(key)) { - throw new ToolRuntimeError("InvalidDataValue", `${label} contains blocked property '${key}'.`) - } const next = copy(item, label, mode, depth + 1, seen) if (next === undefined && mode === "json") continue - copied[key] = next + define(copied, key, next) } seen.delete(value) return copied } + +// Own data property regardless of the target's prototype, so a "__proto__" key on a host object or +// array never reaches the Object.prototype setter. +const define = (target: object, key: string, value: unknown): void => { + Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true }) +} diff --git a/packages/codemode/src/interpreter/methods.ts b/packages/codemode/src/interpreter/methods.ts index 75f8867efe0..dcf142a0e3d 100644 --- a/packages/codemode/src/interpreter/methods.ts +++ b/packages/codemode/src/interpreter/methods.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { isBlockedMember, type SafeObject, toProgram } from "../data.js" +import { type SafeObject, toProgram } from "../data.js" import { dateSetterArgumentCount, invokeDateMethod } from "../stdlib/date.js" import { invokeNumberMethod } from "../stdlib/number.js" import { invokeRegExpMethod, matchToValue, toHostRegex } from "../stdlib/regexp.js" @@ -280,7 +280,7 @@ const invokeStringReplacer = ( if (hasGroups) { const safeGroups: SafeObject = Object.create(null) as SafeObject for (const [key, group] of Object.entries(groups)) { - if (!isBlockedMember(key)) safeGroups[key] = group + safeGroups[key] = group } callbackArgs[callbackArgs.length - 1] = safeGroups } diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index 6a0dcb58aa0..8651a1960ce 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -41,7 +41,7 @@ import type { YieldExpression, } from "acorn" import { Cause, Deferred, Effect, Exit } from "effect" -import { isBlockedMember, ToolRuntimeError, type SafeObject, toProgram } from "../data.js" +import { ToolRuntimeError, type SafeObject, toProgram } from "../data.js" import { ToolReference } from "../tool-runtime.js" import { type AstNode, @@ -1042,7 +1042,7 @@ class Frame { if (property.type === "RestElement") { const rest: SafeObject = Object.create(null) as SafeObject for (const [key, item] of Object.entries(value as SafeObject)) { - if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item + if (!consumed.has(key)) rest[key] = item } copyIteratorSymbols(value, rest, consumed) yield* self.declarePattern(property.argument, rest, mutable, property, initialize) @@ -1050,9 +1050,6 @@ class Frame { } const key = yield* self.destructuringPropertyKey(property) - if (isBlockedMember(String(key))) { - throw new InterpreterRuntimeError(`Property '${String(key)}' is not available.`, property) - } consumed.add(typeof key === "symbol" ? key : String(key)) yield* self.declarePattern( property.value, @@ -1109,16 +1106,13 @@ class Frame { if (property.type === "RestElement") { const rest: SafeObject = Object.create(null) as SafeObject for (const [key, item] of Object.entries(source)) { - if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item + if (!consumed.has(key)) rest[key] = item } copyIteratorSymbols(source, rest, consumed) yield* self.assignPattern(property.argument, rest, property) continue } const key = yield* self.destructuringPropertyKey(property) - if (isBlockedMember(String(key))) { - throw new InterpreterRuntimeError(`Property '${String(key)}' is not available.`, property) - } consumed.add(typeof key === "symbol" ? key : String(key)) yield* self.assignPattern(property.value, self.destructuringPropertyValue(source, key), property) } @@ -1908,10 +1902,7 @@ class Frame { "InvalidDataValue", ) } - for (const [key, value] of Object.entries(spread)) { - if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, property) - objectValue[key] = value - } + for (const [key, value] of Object.entries(spread)) objectValue[key] = value copyIteratorSymbols(spread, objectValue) continue } @@ -1934,9 +1925,6 @@ class Frame { throw new InterpreterRuntimeError("Unsupported object property key shape.", keyNode) } - if (isBlockedMember(String(key))) { - throw new InterpreterRuntimeError(`Property '${String(key)}' is not available.`, keyNode) - } Reflect.set(objectValue, key, yield* self.evaluateExpression(property.value)) } @@ -2051,9 +2039,6 @@ class Frame { } if (objectValue instanceof HostFunction || objectValue instanceof HostNamespace) { - if (typeof key === "string" && isBlockedMember(key)) { - throw new InterpreterRuntimeError(`${objectValue.name}.${key} is not available.`, propertyNode) - } // Unknown static members read as undefined so feature detection works like native JS. return new ComputedValue(objectValue.member(key, propertyNode)) } @@ -2146,10 +2131,6 @@ class Frame { throw new InterpreterRuntimeError("Cannot access a property on a non-object value.", objectNode) } - if (typeof key === "string" && isBlockedMember(key)) { - throw new InterpreterRuntimeError(`Property '${key}' is not available.`, propertyNode) - } - if (Array.isArray(objectValue)) { if (operation === "delete") return { target: objectValue, key } const index = typeof key === "symbol" ? undefined : parseArrayIndex(key) diff --git a/packages/codemode/src/openapi/spec.ts b/packages/codemode/src/openapi/spec.ts index f1ec41ceb86..06283dbf64f 100644 --- a/packages/codemode/src/openapi/spec.ts +++ b/packages/codemode/src/openapi/spec.ts @@ -1,6 +1,5 @@ import { fromSchemaOpenApi3_0, fromSchemaOpenApi3_1 } from "effect/JsonSchema" import type { JsonSchema } from "../tool.js" -import { isBlockedMember } from "../data.js" import type { Body, Document, @@ -468,8 +467,7 @@ export const operationInput = ( ok: true, value: { fields: fields.map((field) => { - const visibleName = isBlockedMember(field.name) ? `${field.name}_2` : field.name - const base = conflicts.has(field.name) ? `${field.location}_${visibleName}` : visibleName + const base = conflicts.has(field.name) ? `${field.location}_${field.name}` : field.name const next = (index: number): string => { const candidate = index === 1 ? base : `${base}_${index}` return used.has(candidate) ? next(index + 1) : candidate @@ -567,12 +565,12 @@ export const operationOutput = ( } const sanitizeOperationSegment = (raw: string): string => { - const base = + return ( raw .replaceAll(/[^A-Za-z0-9_$]+/g, "_") .replace(/^_+|_+$/g, "") .replace(/^([0-9])/, "_$1") || "operation" - return isBlockedMember(base) ? `${base}_2` : base + ) } const fallbackOperationId = (method: string, path: string): string => diff --git a/packages/codemode/src/stdlib/collections.ts b/packages/codemode/src/stdlib/collections.ts index e1854efdcbc..1a27433cc06 100644 --- a/packages/codemode/src/stdlib/collections.ts +++ b/packages/codemode/src/stdlib/collections.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { isBlockedMember, type SafeObject } from "../data.js" +import type { SafeObject } from "../data.js" import { HostFunction, requiresNew } from "../interpreter/host.js" import { type AstNode, InterpreterRuntimeError, isRecord } from "../interpreter/model.js" import { describeValue, isRuntimeReference } from "../interpreter/references.js" @@ -124,12 +124,6 @@ export const groupBy = (runner: Runner, namespace: "Map" | "Object") => cursor, Effect.flatMap(apply([item, index]), (value) => coerceGroupByPropertyKey(runner, value, node)), ) - if (isBlockedMember(key)) { - return yield* preserveConsumerError( - cursor, - Effect.fail(new InterpreterRuntimeError(`Property '${key}' is not available.`, node)), - ) - } const group = result[key] if (group === undefined) result[key] = [item] else (group as Array).push(item) diff --git a/packages/codemode/src/stdlib/json.ts b/packages/codemode/src/stdlib/json.ts index d6c58facd52..e8789ca94ba 100644 --- a/packages/codemode/src/stdlib/json.ts +++ b/packages/codemode/src/stdlib/json.ts @@ -6,15 +6,6 @@ import { typeofValue } from "../interpreter/references.js" import { fromData, type SafeObject, toData, toProgram } from "../data.js" import { Values } from "../values.js" -export const invokeJsonMethod = ( - runner: Runner, - name: "parse" | "stringify", - args: Array, - node: AstNode, -): Effect.Effect => { - return name === "parse" ? parse(runner, args, node) : stringify(runner, args, node) -} - export const jsonGlobal = (runner: Runner) => new HostNamespace("JSON", { parse: new HostFunction({ name: "JSON.parse", call: (args, node) => parse(runner, args, node) }), diff --git a/packages/codemode/src/stdlib/object.ts b/packages/codemode/src/stdlib/object.ts index 5e234e458e9..44da0c3be48 100644 --- a/packages/codemode/src/stdlib/object.ts +++ b/packages/codemode/src/stdlib/object.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { isBlockedMember, toProgram } from "../data.js" +import { toProgram } from "../data.js" import { HostFunction, sync, syncCall } from "../interpreter/host.js" import { type AstNode, AsyncIteratorSymbol, InterpreterRuntimeError, IteratorSymbol } from "../interpreter/model.js" import { @@ -36,8 +36,6 @@ export const objectAssign = (args: Array, node: AstNode): unknown => { const out = target as Record const seen = new Set() const guardedSet = (key: PropertyKey, item: unknown): void => { - if (typeof key === "string" && isBlockedMember(key)) - throw new InterpreterRuntimeError(`Property '${key}' is not available.`, node) rejectCircularInsertion(out, item, "Object.assign result", node, seen) if (!Reflect.set(out, key, item)) throw new InterpreterRuntimeError(`Object.assign could not assign property '${String(key)}'.`, node).as( @@ -95,7 +93,6 @@ const objectFromEntries = ( toProgram(entry[0], "Object.fromEntries key") toProgram(entry[1], "Object.fromEntries value") const key = coerceToString(entry[0]) - if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, node) out[key] = entry[1] }), ) @@ -105,7 +102,7 @@ const objectFromEntries = ( const constructObject = (args: Array, node: AstNode): unknown => { const first = args[0] - if (first === null || first === undefined) return {} + if (first === null || first === undefined) return Object.create(null) if (typeof first === "object") return first throw new InterpreterRuntimeError( `Object(${typeof first}) wrapper objects are not supported; use the primitive value directly.`, diff --git a/packages/codemode/src/stdlib/regexp.ts b/packages/codemode/src/stdlib/regexp.ts index 1c87e84dc7e..b17df75fe69 100644 --- a/packages/codemode/src/stdlib/regexp.ts +++ b/packages/codemode/src/stdlib/regexp.ts @@ -1,6 +1,6 @@ import { sync, syncCall } from "../interpreter/host.js" import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" -import { isBlockedMember, type SafeObject } from "../data.js" +import type { SafeObject } from "../data.js" import { Values } from "../values.js" import { coerceToNumber, coerceToString } from "./value.js" @@ -62,7 +62,7 @@ export const matchToValue = (match: RegExpMatchArray): Array => { if (match.groups) { const groups: SafeObject = Object.create(null) as SafeObject for (const [key, group] of Object.entries(match.groups)) { - if (!isBlockedMember(key)) groups[key] = group + groups[key] = group } result.groups = groups } @@ -148,7 +148,7 @@ const indicesToValue = (indices: RegExpIndicesArray): IndicesValue => { if (indices.groups) { const groups: SafeObject = Object.create(null) as SafeObject for (const [key, range] of Object.entries(indices.groups)) { - if (!isBlockedMember(key)) groups[key] = range === undefined ? undefined : [...range] + groups[key] = range === undefined ? undefined : [...range] } result.groups = groups return result diff --git a/packages/codemode/test/json-callbacks-test262.test.ts b/packages/codemode/test/json-callbacks-test262.test.ts index 4534e1837b1..6f1596e88ca 100644 --- a/packages/codemode/test/json-callbacks-test262.test.ts +++ b/packages/codemode/test/json-callbacks-test262.test.ts @@ -29,7 +29,6 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" import { CodeMode } from "../src/index.js" -import { invokeJsonMethod } from "../src/stdlib/json.js" const value = async (code: string) => { const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) @@ -218,27 +217,19 @@ describe("CodeMode JSON callback boundaries", () => { expect(result).toMatchObject({ ok: false, error: { kind: "UnsupportedSyntax" } }) }) - test("blocked parse keys are rejected before reviver traversal", async () => { + test("prototype-named keys parse as own data and reach the reviver", async () => { expect( - await value( - `try { JSON.parse('{"__proto__":1}', (key, item) => item) } catch (error) { return true } return false`, - ), - ).toBe(true) + await value(` + const seen = [] + const parsed = JSON.parse('{"__proto__":{"polluted":1},"constructor":2}', (key, item) => { seen.push(key); return item }) + return [seen, parsed.__proto__.polluted, parsed.constructor, ({}).polluted, Object.keys(parsed)] + `), + ).toEqual([["polluted", "__proto__", "constructor", ""], 1, 2, null, ["__proto__", "constructor"]]) }) - test("JSON.stringify directly rejects blocked input keys", () => { - expect(() => - invokeJsonMethod( - { - invokeFunction: () => Effect.die("unused"), - invokeCallable: () => Effect.die("unused"), - settlePromise: () => Effect.die("unused"), - syncIterator: () => Effect.die("unused"), - }, - "stringify", - [Object.fromEntries([["constructor", 1]])], - { type: "CallExpression", start: 0, end: 0 }, - ), - ).toThrow("blocked property 'constructor'") + test("JSON.stringify serializes prototype-named own keys", async () => { + expect(await value(`return JSON.stringify({ constructor: 1, __proto__: 2 })`)).toBe( + '{"constructor":1,"__proto__":2}', + ) }) }) diff --git a/packages/codemode/test/openapi.test.ts b/packages/codemode/test/openapi.test.ts index ff59fd90094..01f87c9c622 100644 --- a/packages/codemode/test/openapi.test.ts +++ b/packages/codemode/test/openapi.test.ts @@ -1095,7 +1095,7 @@ describe("OpenAPI.fromSpec", () => { tags: ["x", "y"], filter: { state: "open", page: 2 }, nullable: null, - constructor_2: "safe", + constructor: "safe", meta: { a: "b", c: "d" }, }) .pipe(Effect.provide(client.layer)), diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index da63bc01164..9fc13debe04 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -265,9 +265,11 @@ describe("property deletion", () => { expect((await error(`return delete tools.example`)).kind).toBe("InvalidDataValue") }) - test("keeps blocked property names unavailable", async () => { - expect((await error(`const object = {}; return delete object.__proto__`)).kind).toBe("ExecutionFailure") - expect((await error(`const values = []; return delete values["constructor"]`)).kind).toBe("ExecutionFailure") + test("prototype-named keys delete like any own data key", async () => { + expect( + await value(`const object = { __proto__: 1, a: 2 }; delete object.__proto__; return Object.keys(object)`), + ).toEqual(["a"]) + expect(await value(`const values = [1]; delete values["constructor"]; return values`)).toEqual([1]) }) }) @@ -906,10 +908,12 @@ describe("coercion parity: unknown static members read as undefined", () => { ) }) - test("blocked members still throw instead of reading as undefined", async () => { - const err = await error(`return Math.constructor`) - expect(err.message).toContain("not available") - const coercionErr = await error(`return Number.constructor`) - expect(coercionErr.message).toContain("Number.constructor is not available") + test("prototype-named members on globals read as undefined like other unknown statics", async () => { + expect(await value(`return [Math.constructor, Number.constructor, Object.prototype, Array.__proto__]`)).toEqual([ + null, + null, + null, + null, + ]) }) }) diff --git a/packages/codemode/test/regexp-math-test262.test.ts b/packages/codemode/test/regexp-math-test262.test.ts index 5fd2100a378..bd85d3ef2db 100644 --- a/packages/codemode/test/regexp-math-test262.test.ts +++ b/packages/codemode/test/regexp-math-test262.test.ts @@ -126,7 +126,7 @@ describe("RegExp Test262 parity", () => { const match = /(?a)(b)?/d.exec("a") const stringMatch = "a".match(/a/d) const all = "a a".matchAll(/a/dg) - const blocked = /(?a)(?b)/d.exec("ab") + const named = /(?a)(?b)/d.exec("ab") return [ /./.hasIndices, /./d.hasIndices, @@ -138,10 +138,10 @@ describe("RegExp Test262 parity", () => { stringMatch.indices[0], all[0].indices[0], all[1].indices[0], - Object.keys(blocked.indices.groups), + Object.keys(named.indices.groups), ] `), - ).toEqual([false, true, true, [0, 1], [0, 1], null, [0, 1], [0, 1], [0, 1], [2, 3], ["safe"]]) + ).toEqual([false, true, true, [0, 1], [0, 1], null, [0, 1], [0, 1], [0, 1], [2, 3], ["constructor", "safe"]]) }) test("match indices preserve captures, Unicode offsets, and groups properties", async () => { @@ -177,13 +177,13 @@ describe("RegExp Test262 parity", () => { ]) }) - test("match and matchAll preserve named, unmatched, and blocked index groups", async () => { + test("match and matchAll preserve named, unmatched, and prototype-named index groups", async () => { expect( await value(` const matched = "a".match(/(?a)|(?x)/d).indices.groups const all = "a x".matchAll(/(?a)|(?x)/dg) - const blockedMatch = "ab".match(/(?a)(?b)/d).indices.groups - const blockedAll = "ab".matchAll(/(?a)(?b)/dg)[0].indices.groups + const namedMatch = "ab".match(/(?a)(?b)/d).indices.groups + const namedAll = "ab".matchAll(/(?a)(?b)/dg)[0].indices.groups return [ matched.a, matched.x, @@ -191,11 +191,11 @@ describe("RegExp Test262 parity", () => { all[0].indices.groups.x, all[1].indices.groups.a, all[1].indices.groups.x, - Object.keys(blockedMatch), - Object.keys(blockedAll), + Object.keys(namedMatch), + Object.keys(namedAll), ] `), - ).toEqual([[0, 1], null, [0, 1], null, null, [2, 3], ["safe"], ["safe"]]) + ).toEqual([[0, 1], null, [0, 1], null, null, [2, 3], ["constructor", "safe"], ["constructor", "safe"]]) }) test("v flag exposes unicodeSets and remains exclusive with u", async () => { diff --git a/packages/codemode/test/tool-paths.test.ts b/packages/codemode/test/tool-paths.test.ts index 33af736756a..f1b00f85e99 100644 --- a/packages/codemode/test/tool-paths.test.ts +++ b/packages/codemode/test/tool-paths.test.ts @@ -176,10 +176,31 @@ describe("blocked member names on tool paths", () => { expect(await value(poisoned, `return await tools.ns.real({})`)).toBe("real") }) - test("blocked member access on data values stays blocked", async () => { - const diagnostic = await failure(runtime, `const x = {}; return x.constructor`) - expect(diagnostic.message).toContain("constructor") + test("prototype machinery is unreachable through data values", async () => { + expect( + await value( + runtime, + ` + const object = {} + const array = [] + object.__proto__ = { polluted: true } + return [ + object.constructor, array.constructor, "".constructor, Math.constructor, + object.__proto__.polluted, ({}).polluted, array.__proto__, Object().__proto__, new Object().constructor, + typeof ({}).constructor, typeof [].__proto__, + ] + `, + ), + ).toEqual([null, null, null, null, true, null, null, null, null, "undefined", "undefined"]) + expect((await failure(runtime, `return (() => 1).constructor`)).message).toContain( + "Cannot read properties of a function", + ) + const escape = await failure(runtime, `return ({}).constructor.constructor("return 1")()`) + expect(escape.message).toContain("Cannot access a property on a non-object value") + const poisoned = await failure(runtime, `const o = {}; o.__proto__.constructor("return 1")`) + expect(poisoned.message).toContain("Cannot access a property on a non-object value") expect(Object.keys(Object.prototype)).toEqual([]) + expect(Object.keys(Array.prototype)).toEqual([]) }) })