mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-10 07:11:25 +03:00
fix(codemode): strip __proto__ keys at host object boundaries (#48926)
This commit is contained in:
committed by
GitHub
Unverified
parent
0a3dc16961
commit
3045156fa6
@@ -20,7 +20,9 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] Top-level `await` and `return` through the program's implicit async-function scope.
|
||||
- [x] Explicit `return`, final top-level expression as a REPL-style result, and `null` when no value is produced.
|
||||
- [x] Program results use JSON-like boundaries, with `undefined` and non-finite numbers normalized to `null`. Tool
|
||||
arguments follow JSON serialization semantics before their schema applies (see the tools section).
|
||||
arguments follow JSON serialization semantics before their schema applies (see the tools section). Own
|
||||
`__proto__` keys are dropped wherever a host object crosses to the host, so merging tool inputs or results
|
||||
cannot replace a prototype; `JSON.stringify` still emits the key, like JS, since a string cannot pollute.
|
||||
- [x] Live Date, RegExp, Map, Set, URL, and URLSearchParams values inside CodeMode.
|
||||
- [x] Tool calls through the host-provided `tools` tree only.
|
||||
- [x] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is
|
||||
|
||||
@@ -59,8 +59,12 @@ export const fromData = (protos: Prototypes, value: unknown, label: string): unk
|
||||
* dropped ("json") or become null ("result", for program results where the consumer must never see
|
||||
* undefined); a bare `undefined` follows the same rule.
|
||||
*/
|
||||
export const toData = (value: unknown, label: string, undefinedAs: "json" | "result" = "json"): unknown =>
|
||||
copy(value, label, undefinedAs, 0, new Set())
|
||||
export const toData = (
|
||||
value: unknown,
|
||||
label: string,
|
||||
undefinedAs: "json" | "result" = "json",
|
||||
stripProto = true,
|
||||
): unknown => copy(value, label, undefinedAs, 0, new Set(), undefined, stripProto)
|
||||
|
||||
// "program" and "data" build program objects; "json" and "result" build ordinary objects for the host.
|
||||
type Mode = "program" | "data" | "json" | "result"
|
||||
@@ -72,8 +76,9 @@ const copy = (
|
||||
depth: number,
|
||||
seen: Set<object>,
|
||||
protos?: Prototypes,
|
||||
stripProto = true,
|
||||
): unknown => {
|
||||
const next = (item: unknown) => copy(item, label, mode, depth + 1, seen, protos)
|
||||
const next = (item: unknown) => copy(item, label, mode, depth + 1, seen, protos, stripProto)
|
||||
if (depth > MAX_VALUE_DEPTH) {
|
||||
throw new ToolRuntimeError("InvalidDataValue", `${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`)
|
||||
}
|
||||
@@ -145,6 +150,7 @@ const copy = (
|
||||
defineHost(copied, "message", next(get(value, "message")))
|
||||
}
|
||||
for (const [key, item] of entries(value)) {
|
||||
if (stripProto && key === "__proto__") continue
|
||||
const copiedItem = next(item)
|
||||
if (copiedItem === undefined && mode === "json") continue
|
||||
defineHost(copied, key, copiedItem)
|
||||
@@ -180,6 +186,7 @@ const copy = (
|
||||
}
|
||||
const copied: Record<string, unknown> = {}
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
if (stripProto && key === "__proto__") continue
|
||||
const copiedItem = next(item)
|
||||
if (copiedItem === undefined && mode === "json") continue
|
||||
defineHost(copied, key, copiedItem)
|
||||
|
||||
@@ -56,7 +56,8 @@ const stringify = <R>(runner: Runner<R>, args: Array<unknown>): Effect.Effect<un
|
||||
.filter((item): item is string | number => typeof item === "string" || typeof item === "number")
|
||||
.map(String)
|
||||
: null
|
||||
return Effect.succeed(JSON.stringify(toData(args[0], "JSON.stringify value"), properties, indent))
|
||||
// A string cannot pollute, so __proto__ stays: JSON.stringify includes own __proto__ keys, like JS.
|
||||
return Effect.succeed(JSON.stringify(toData(args[0], "JSON.stringify value", "json", false), properties, indent))
|
||||
}
|
||||
|
||||
// Validate up front; the replacer walk below reads the original value.
|
||||
|
||||
@@ -198,7 +198,9 @@ describe("blocked member names on tool paths", () => {
|
||||
const poisoned = await failure(runtime, `const o = {}; o.__proto__.constructor("return 1")`)
|
||||
expect(poisoned.message).toContain("Cannot read properties of undefined")
|
||||
// Prototype mutation is confined to one run: the next program starts from fresh intrinsics.
|
||||
expect(await value(runtime, `Object.prototype.polluted = 1; Array.prototype.push = 2; return ({}).polluted`)).toBe(1)
|
||||
expect(await value(runtime, `Object.prototype.polluted = 1; Array.prototype.push = 2; return ({}).polluted`)).toBe(
|
||||
1,
|
||||
)
|
||||
expect(await value(runtime, `return [({}).polluted, typeof [].push]`)).toEqual([null, "function"])
|
||||
expect(Object.keys(Object.prototype)).toEqual([])
|
||||
expect(Object.keys(Array.prototype)).toEqual([])
|
||||
@@ -279,3 +281,34 @@ describe("canonical path collisions", () => {
|
||||
expect(await value(runtime, `return await tools.issues.close({})`)).toBe("closed")
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool argument prototype safety", () => {
|
||||
test("a __proto__ key never reaches tool code", async () => {
|
||||
let seen: unknown
|
||||
const runtime = CodeMode.make({
|
||||
tools: {
|
||||
inspect: Tool.make({
|
||||
description: "Inspect",
|
||||
input: Schema.Struct({ v: Schema.Unknown }),
|
||||
output: Schema.Unknown,
|
||||
execute: (input) =>
|
||||
Effect.sync(() => {
|
||||
seen = (input as { v: unknown }).v
|
||||
return null
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
await value(runtime, `return await tools.inspect({ v: { __proto__: { polluted: true }, a: 1 } })`)
|
||||
expect(seen).toEqual({ a: 1 })
|
||||
const merged = Object.assign({}, seen as Record<string, unknown>)
|
||||
expect(merged.polluted).toBeUndefined()
|
||||
expect(Object.getPrototypeOf(merged)).toBe(Object.prototype)
|
||||
})
|
||||
|
||||
test("program results drop __proto__ keys", async () => {
|
||||
const runtime = CodeMode.make({ tools: {} })
|
||||
expect(await value(runtime, `return { __proto__: { polluted: true }, a: 2 }`)).toEqual({ a: 2 })
|
||||
expect(await value(runtime, `return [{ __proto__: 1 }]`)).toEqual([{}])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user