mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 18:05:37 +08:00
cd17912f04
`JSON.parse('{"__proto__":{…}}')` produces `__proto__` as an *own
enumerable* property, so `Object.entries` yields it; and
`isPlainObject(Object.prototype)` is true, so `deepMerge` skipped its
"replace with empty object" branch and merged straight into the global
prototype. Reproduced, not inferred.
`deepRemove` had the same shape and was destructive: `"__proto__" in
target` is always true because `in` walks the prototype chain, so it
recursed into `Object.prototype` and deleted from it.
Reachable without XSS: `settings` is not in the sync skip/preserve lists,
so `common_config_*` is overwritten by whatever the WebDAV/S3 remote
sends, and opening a provider form merges it.
Guard all three walkers that share the traversal shape. The third,
`isSubset`, only reads and cannot pollute, but following
`target["__proto__"]` made `{"__proto__":{}}` a subset of *every* config,
so the "common config applied" toggle read wrong. It also now requires
own properties, since an inherited key is not "present in the config".
`isSubset` rejects on a forbidden key rather than skipping: if a future
caller bypasses sanitization, reporting "not applied" is the safe
direction because re-applying is idempotent.
That rejection alone left an inconsistency: merge skips forbidden keys
and keeps going, so `{"env":{"A":"1"},"__proto__":{}}` really did write
`env.A` while `hasCommonConfigSnippet` reported it as never applied.
Fixed by sanitizing on the *reading* side only, so the comparison runs
against exactly what the write side produces. Deliberately not applied to
the write path: `deepMerge`/`deepRemove` already skip these keys, so
sanitizing first is byte-for-byte identical there -- an unfalsifiable
call that would wrongly imply the walkers cannot handle their own input.
`deepCloneFallback` gets the same skip. Its impact differs and the
comment says so: it does not reach the global prototype, it swaps the
clone's own prototype, giving the copy ghost properties. It is dead while
`structuredClone` exists, but the two paths disagreed on `__proto__`.
28 lines
1.1 KiB
TypeScript
28 lines
1.1 KiB
TypeScript
export function deepClone<T>(value: T): T {
|
|
if (typeof globalThis.structuredClone === "function") {
|
|
return globalThis.structuredClone(value);
|
|
}
|
|
|
|
return deepCloneFallback(value);
|
|
}
|
|
|
|
function deepCloneFallback<T>(value: T): T {
|
|
if (value === null || typeof value !== "object") return value;
|
|
if (value instanceof Date) return new Date(value.getTime()) as T;
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => deepCloneFallback(item)) as T;
|
|
}
|
|
|
|
const cloned = {} as T;
|
|
Object.keys(value).forEach((key) => {
|
|
// `cloned["__proto__"] = …` 走的是 setter,会替换 cloned 自己的原型而不是
|
|
// 新增一个自有属性。这不会污染全局 `Object.prototype`(已实测),但会让克隆体
|
|
// 凭空读得到源对象里那些键,是个难查的幽灵属性。`structuredClone` 把
|
|
// `__proto__` 当普通数据键原样保留,两条路径的行为因此不一致——跳过它,
|
|
// 让 fallback 与主路径对齐。
|
|
if (key === "__proto__") return;
|
|
cloned[key as keyof T] = deepCloneFallback(value[key as keyof T]);
|
|
});
|
|
return cloned;
|
|
}
|