mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 05:31:38 +00:00
A plugin manifest configSchema declaring a __proto__ property polluted Object.prototype process-wide during ordinary plugin config validation. applyObjectPropertyDefaults iterated schema-controlled property keys and wrote into value[key] with no blocked-key guard; because manifests are parsed JSON, __proto__ is an own key and value["__proto__"] dereferences the inherited Object.prototype, onto which nested defaults were written. Skip __proto__, prototype, and constructor at each write site in applyObjectPropertyDefaults, reusing the existing isBlockedObjectKey helper already used at other untrusted object-write boundaries.
96 lines
3.0 KiB
TypeScript
96 lines
3.0 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
applyJsonSchemaDefaults,
|
|
findJsonSchemaShapeError,
|
|
normalizeJsonSchemaForTypeBox,
|
|
} from "./json-schema-defaults.js";
|
|
|
|
describe("normalizeJsonSchemaForTypeBox", () => {
|
|
it("combines pattern properties that collide after unicode repair", () => {
|
|
const normalized = normalizeJsonSchemaForTypeBox({
|
|
type: "object",
|
|
patternProperties: {
|
|
"^https:": { minLength: 1 },
|
|
"^https\\:": { maxLength: 10 },
|
|
},
|
|
});
|
|
|
|
expect(normalized).toMatchObject({
|
|
patternProperties: {
|
|
"^https:": {
|
|
allOf: [{ minLength: 1 }, { maxLength: 10 }],
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it.each(["constructor", "toString", "__proto__"])(
|
|
"preserves pattern property key %s",
|
|
(pattern) => {
|
|
const normalized = normalizeJsonSchemaForTypeBox({
|
|
type: "object",
|
|
patternProperties: Object.fromEntries([[pattern, { type: "string" }]]),
|
|
});
|
|
|
|
expect(normalized).toMatchObject({
|
|
patternProperties: Object.fromEntries([[pattern, { type: "string" }]]),
|
|
});
|
|
},
|
|
);
|
|
|
|
it("resolves local refs to array entries beyond config path index limits", () => {
|
|
const prefixItems: (boolean | { type: string })[] = Array.from({ length: 100_002 }, () => true);
|
|
prefixItems[100_001] = { type: "string" };
|
|
|
|
expect(
|
|
findJsonSchemaShapeError({
|
|
type: "array",
|
|
prefixItems,
|
|
items: { $ref: "#/prefixItems/100001" },
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("applyJsonSchemaDefaults prototype safety", () => {
|
|
const readPollution = () => (Object.prototype as Record<string, unknown>).polluted;
|
|
|
|
afterEach(() => {
|
|
delete (Object.prototype as Record<string, unknown>).polluted;
|
|
});
|
|
|
|
it("does not pollute Object.prototype through a __proto__ property schema", () => {
|
|
const schema = JSON.parse(
|
|
'{"type":"object","properties":{"__proto__":{"type":"object","properties":{"polluted":{"default":"yes"}}}}}',
|
|
);
|
|
|
|
const result = applyJsonSchemaDefaults(schema, {});
|
|
|
|
expect(readPollution()).toBeUndefined();
|
|
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
|
|
expect(Object.hasOwn(result, "polluted")).toBe(false);
|
|
});
|
|
|
|
it("does not pollute Object.prototype through a __proto__ pattern property schema", () => {
|
|
const schema = JSON.parse(
|
|
'{"type":"object","patternProperties":{".*":{"type":"object","properties":{"polluted":{"default":"yes"}}}}}',
|
|
);
|
|
const value = JSON.parse('{"__proto__":{}}');
|
|
|
|
applyJsonSchemaDefaults(schema, value);
|
|
|
|
expect(readPollution()).toBeUndefined();
|
|
});
|
|
|
|
it("does not pollute Object.prototype through a __proto__ additional property schema", () => {
|
|
const schema = JSON.parse(
|
|
'{"type":"object","additionalProperties":{"type":"object","properties":{"polluted":{"default":"yes"}}}}',
|
|
);
|
|
const value = JSON.parse('{"__proto__":{}}');
|
|
|
|
applyJsonSchemaDefaults(schema, value);
|
|
|
|
expect(readPollution()).toBeUndefined();
|
|
});
|
|
});
|