mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-05 23:41:42 +00:00
fix(shared): block prototype-polluting keys in JSON schema default hydration (#107978)
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.
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { findJsonSchemaShapeError, normalizeJsonSchemaForTypeBox } from "./json-schema-defaults.js";
|
||||
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", () => {
|
||||
@@ -47,3 +51,45 @@ describe("normalizeJsonSchemaForTypeBox", () => {
|
||||
).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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
// JSON schema default helpers fill object values from TypeBox schema defaults.
|
||||
import { Compile } from "typebox/compile";
|
||||
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
|
||||
import type { JsonSchemaObject } from "./json-schema.types.js";
|
||||
|
||||
type JsonSchemaValue = JsonSchemaObject | boolean;
|
||||
@@ -926,6 +927,9 @@ function applyObjectPropertyDefaults(
|
||||
): Record<string, unknown> {
|
||||
const properties = isRecord(schema.properties) ? schema.properties : {};
|
||||
for (const [key, propertySchema] of Object.entries(properties)) {
|
||||
if (isBlockedObjectKey(key)) {
|
||||
continue;
|
||||
}
|
||||
const currentValue = value[key];
|
||||
const defaultedValue = applySchemaDefaults(
|
||||
propertySchema as JsonSchemaValue,
|
||||
@@ -951,7 +955,7 @@ function applyObjectPropertyDefaults(
|
||||
continue;
|
||||
}
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!regex.test(key)) {
|
||||
if (isBlockedObjectKey(key) || !regex.test(key)) {
|
||||
continue;
|
||||
}
|
||||
patternMatchedKeys.add(key);
|
||||
@@ -969,7 +973,11 @@ function applyObjectPropertyDefaults(
|
||||
if (isRecord(schema.additionalProperties)) {
|
||||
const additionalSchema = schema.additionalProperties as JsonSchemaValue;
|
||||
for (const key of Object.keys(value)) {
|
||||
if (Object.hasOwn(properties, key) || patternMatchedKeys.has(key)) {
|
||||
if (
|
||||
isBlockedObjectKey(key) ||
|
||||
Object.hasOwn(properties, key) ||
|
||||
patternMatchedKeys.has(key)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
value[key] = applySchemaDefaults(
|
||||
|
||||
Reference in New Issue
Block a user