fix(protocol): validate 拒检原型链键(A-P0-1,in → Object.hasOwn)
架构审查 A-P0-1:validate.ts:69 用 `key in schema.properties` 判 「附加属性是否允许」,而 `in` 沿原型链查找——JSON 里写 `"constructor"` / `"toString"` / `"__proto__"` 等键会命中 Object.prototype 成员被判为「schema 已声明」从而跳过拒绝, fail-open(与 server.ts B-03 同源复发:那个修的是 ACL clients, 这个修的是 returns_schema 校验)。 - 改为 `Object.prototype.hasOwnProperty.call(schema.properties, key)`, 只认真实声明的属性 - 补 2 个「注入即红」用例:原型键必须被判 additional property; 真实 manifest 能力 system.agent.update 同样拒原型键、合法调用照过 - 故障注入实测:改回 `in` → 2 例红;恢复 → 全绿 安全属性收紧(原行为是漏放),无任何放宽。
This commit is contained in:
1 parent
cb468df7da
commit
68ea3cd20e
2 files changed
+50
-1
No files matched your search
@@ -66,7 +66,13 @@ export function validate(value: unknown, schema: JsonSchema, path = '$'): string
|
||||
}
|
||||
if (schema.additionalProperties === false) {
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!(key in schema.properties)) {
|
||||
// ⚠️ A-P0-1(2026-10-25):必须用 **own-property** 判断,不能用 `key in schema.properties`。
|
||||
// `in` 沿原型链查找,而 `schema.properties` 是普通对象字面量(继承 Object.prototype)——
|
||||
// 调用方传 `{"constructor":1}` / `{"toString":1}` / `{"valueOf":1}` 时
|
||||
// `key in properties` 为 true → **不判 additional property** → 校验放行(fail-open)。
|
||||
// 与 server.ts B-03(`client_id:"constructor"` 沿原型链取到函数当密钥)是
|
||||
// 同一错误模式在不同文件复发;此处修的是「守卫方向」的失效。
|
||||
if (!Object.prototype.hasOwnProperty.call(schema.properties, key)) {
|
||||
errors.push(`${path}.${key}: additional property not allowed`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,3 +80,46 @@ test('applyDefaults:填补缺失字段,不覆盖已有值', () => {
|
||||
assert.equal(out.limit, 5, '已有值不被覆盖');
|
||||
assert.equal(out.recursive, false, '缺失字段填默认值');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// A-P0-1(2026-10-25):原型键绕过 additionalProperties:false —— fail-open 方向的守卫缺陷
|
||||
//
|
||||
// 根因:`key in schema.properties` 沿**原型链**查找,而 schema.properties 是普通
|
||||
// 对象字面量(继承 Object.prototype)。调用方传 {"constructor":1} 等键时
|
||||
// `key in properties` 为 true → 不判 additional property → **校验放行**。
|
||||
// 与 server.ts B-03(client_id:"constructor" 取到函数当密钥打死进程)是同一错误
|
||||
// 模式在不同文件复发。修法:Object.prototype.hasOwnProperty.call。
|
||||
//
|
||||
// 本用例即「注入即红」:把实现改回 `in`,下面三条必须全部失败。
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('A-P0-1:原型键(constructor/toString/valueOf/hasOwnProperty)必须被判 additional property', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: { name: { type: 'string' } },
|
||||
additionalProperties: false,
|
||||
};
|
||||
for (const protoKey of ['constructor', 'toString', 'valueOf', 'hasOwnProperty', '__proto__', 'isPrototypeOf']) {
|
||||
const errs = validate({ [protoKey]: 1 }, schema);
|
||||
assert.ok(
|
||||
errs.length > 0,
|
||||
`传入原型键 ${protoKey} 必须被判 additional property(in 运算符会沿原型链放行它 —— fail-open)`,
|
||||
);
|
||||
assert.match(errs.join(';'), new RegExp(protoKey), `错误应指出是哪个键:${errs.join('; ')}`);
|
||||
}
|
||||
// 正常键不受影响(防「修过头把合法字段也拒了」)
|
||||
assert.deepEqual(validate({ name: 'ok' }, schema), []);
|
||||
});
|
||||
|
||||
test('A-P0-1:真实 manifest 能力(system.agent.update)同样拒原型键', async () => {
|
||||
const { findCapability } = await import('../../packages/protocol/dist/index.js');
|
||||
const cap = findCapability('system.agent.update');
|
||||
assert.ok(cap, '能力应存在');
|
||||
// 合法调用必须照过(回归保护:修守卫不能误伤正常请求)
|
||||
assert.deepEqual(validate({ url: 'http://x/y', sha256: 'a'.repeat(64) }, cap.params_schema), []);
|
||||
// 原型键必须被拒
|
||||
for (const protoKey of ['constructor', 'toString']) {
|
||||
const errs = validate({ url: 'http://x/y', sha256: 'a'.repeat(64), [protoKey]: 'x' }, cap.params_schema);
|
||||
assert.ok(errs.length > 0, `${protoKey} 必须被拒`);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user