diff --git a/nemoclaw/src/commands/migration-state.test.ts b/nemoclaw/src/commands/migration-state.test.ts index 70575be66..46efee60c 100644 --- a/nemoclaw/src/commands/migration-state.test.ts +++ b/nemoclaw/src/commands/migration-state.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import type { PluginLogger } from "../index.js"; +import { setConfigValue } from "./migration-state.js"; // --------------------------------------------------------------------------- // fs mock — thin in-memory store keyed by absolute path @@ -1417,4 +1418,37 @@ describe("commands/migration-state", () => { } }); }); + + // ── setConfigValue prototype pollution guard ───────────────────── + + describe("setConfigValue", () => { + it.each(["__proto__", "constructor", "prototype"])( + "rejects unsafe path segment: %s", + (segment) => { + const doc: Record = {}; + expect(() => setConfigValue(doc, `${segment}.polluted`, "true")).toThrow( + /Unsafe config path segment/, + ); + }, + ); + + it("rejects __proto__ in nested position", () => { + const doc: Record = {}; + expect(() => + setConfigValue(doc, `agents.__proto__.isAdmin`, "true"), + ).toThrow(/Unsafe config path segment/); + }); + + it("allows legitimate dotted paths", () => { + const doc: Record = {}; + setConfigValue(doc, "agents.list[0].workspace", "/tmp/ws"); + expect((doc as any).agents.list[0].workspace).toBe("/tmp/ws"); + }); + + it("allows simple top-level keys", () => { + const doc: Record = {}; + setConfigValue(doc, "theme", "dark"); + expect(doc.theme).toBe("dark"); + }); + }); }); diff --git a/nemoclaw/src/commands/migration-state.ts b/nemoclaw/src/commands/migration-state.ts index 4a4345e66..81b6cccd4 100644 --- a/nemoclaw/src/commands/migration-state.ts +++ b/nemoclaw/src/commands/migration-state.ts @@ -588,7 +588,10 @@ function resolveConfigSourcePath(manifest: SnapshotManifest, snapshotDir: string return path.join(snapshotDir, "openclaw", "openclaw.json"); } -function setConfigValue( +const UNSAFE_PROPERTY_NAMES = new Set(["__proto__", "constructor", "prototype"]); + +/** @visibleForTesting */ +export function setConfigValue( document: Record, configPath: string, value: string, @@ -598,6 +601,14 @@ function setConfigValue( throw new Error(`Invalid config path: ${configPath}`); } + for (const token of tokens) { + if (UNSAFE_PROPERTY_NAMES.has(token)) { + throw new Error( + `Unsafe config path segment '${token}' in ${configPath}`, + ); + } + } + let current: unknown = document; for (let index = 0; index < tokens.length - 1; index += 1) { const token = tokens[index];