fix: check only own properties when validating tool arguments

Inherited names such as constructor, toString and __proto__ passed as
declared properties despite additionalProperties: false, and a schema-valued
additionalProperties was accepted but never enforced. Validation now uses own
properties, and definitions must give additionalProperties as a boolean.
This commit is contained in:
Sameh Khamis committed 2026-10-02 15:21:36 -07:00
1 parent c4a4247f70
commit 6a45a8f6a2
2 files changed
+40 -2

No files matched your search

+29
View File
@@ -203,3 +203,32 @@ test('the validator checks every supported keyword', () => {
'arguments.n must be a number',
]);
});
test('inherited names are not declared properties, and schema-valued additionalProperties is refused', () => {
const schema = {
type: 'object',
properties: { name: { type: 'string' } },
additionalProperties: false,
};
for (const name of ['constructor', 'toString', '__proto__'])
assert.deepEqual(validateValue(schema, JSON.parse(`{"${name}": 1}`)), [
`arguments has unknown property ${name}`,
]);
assert.deepEqual(validateValue({ ...schema, required: ['toString'] }, {}), [
'arguments is missing toString',
]);
assert.throws(
() =>
defineTool({
name: 'loose',
title: 'Loose',
description: 'Promises checks it cannot make.',
inputSchema: {
type: 'object',
additionalProperties: { type: 'string' },
},
run: async () => ({ summary: '', data: {} }),
}),
/additionalProperties must be true or false/,
);
});
+11 -2
View File
@@ -44,6 +44,12 @@ export function assertSupportedSchema(schema, path = 'schema') {
if (!TYPES.has(schema.type))
throw new TypeError(`${path} has unsupported type ${schema.type}`);
if (schema.pattern !== undefined) new RegExp(schema.pattern, 'u');
// Only the boolean form is checked; a schema here would go unenforced.
if (
schema.additionalProperties !== undefined &&
typeof schema.additionalProperties !== 'boolean'
)
throw new TypeError(`${path}.additionalProperties must be true or false`);
for (const [name, child] of Object.entries(schema.properties || {}))
assertSupportedSchema(child, `${path}.${name}`);
if (schema.items) assertSupportedSchema(schema.items, `${path}[]`);
@@ -63,11 +69,14 @@ export function validateValue(schema, value, path = 'arguments') {
break;
}
for (const name of schema.required || []) {
if (value[name] === undefined) fail(`is missing ${name}`);
if (!Object.hasOwn(value, name) || value[name] === undefined)
fail(`is missing ${name}`);
}
// Own properties only, so names such as constructor or __proto__ are
// not mistaken for declared ones.
const properties = schema.properties || {};
for (const [name, child] of Object.entries(value)) {
if (properties[name])
if (Object.hasOwn(properties, name))
problems.push(
...validateValue(properties[name], child, `${path}.${name}`),
);