bookworm-smart-assistant/hooks/lib/safe-merge.js

81 lines
2.3 KiB
JavaScript
Raw Permalink Normal View History

'use strict';
/**
* safe-merge.js 原型污染防护 (P1-SAFE-MERGE-V1)
*
* 对齐 OpenClaw 双层架构:
* - infra/prototype-keys.ts (BLOCKED_OBJECT_KEYS)
* - config/merge-patch.ts (applyMergePatch)
* - plugins/provider-auth-choice-helpers.ts (sanitizeConfigPatchValue)
*
* Bookworm 高风险位置:
* - scripts/adaptive-disambiguator.js loadState() 持久化污染
* - hooks/route-interceptor-bundle.js 4 JSON.parse 后展开
*/
const BLOCKED_MERGE_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
function isPlainObject(value) {
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}
function sanitizeValue(value) {
if (Array.isArray(value)) return value.map(sanitizeValue);
if (!isPlainObject(value)) return value;
const next = Object.create(null);
for (const [key, nestedValue] of Object.entries(value)) {
if (BLOCKED_MERGE_KEYS.has(key)) continue;
next[key] = sanitizeValue(nestedValue);
}
return next;
}
function safeMerge(base, patch) {
if (!isPlainObject(patch)) return sanitizeValue(patch);
const result = isPlainObject(base) ? Object.assign({}, base) : {};
for (const [key, value] of Object.entries(patch)) {
if (BLOCKED_MERGE_KEYS.has(key)) continue;
if (value === null) {
delete result[key];
continue;
}
if (isPlainObject(value) && isPlainObject(result[key])) {
result[key] = safeMerge(result[key], value);
} else {
result[key] = sanitizeValue(value);
}
}
return result;
}
function safeJsonParse(text, fallback) {
if (fallback === undefined) fallback = null;
try {
const parsed = JSON.parse(text);
return sanitizeValue(parsed);
} catch (_) {
return fallback;
}
}
function assertNoPollution(obj, depth) {
if (depth === undefined) depth = 10;
if (depth <= 0 || !isPlainObject(obj)) return true;
for (const key of Object.keys(obj)) {
if (BLOCKED_MERGE_KEYS.has(key)) return false;
if (!assertNoPollution(obj[key], depth - 1)) return false;
}
return true;
}
module.exports = {
safeMerge,
safeJsonParse,
sanitizeValue,
assertNoPollution,
isPlainObject,
BLOCKED_MERGE_KEYS,
__sentinel: 'P1-SAFE-MERGE-V1',
};