'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', };