mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-25 23:47:20 +00:00
fix: harden config prototype-key guards (#22968) (thanks @Clawborn)
This commit is contained in:
23
src/config/legacy.shared.test.ts
Normal file
23
src/config/legacy.shared.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { mergeMissing } from "./legacy.shared.js";
|
||||
|
||||
describe("mergeMissing prototype pollution guard", () => {
|
||||
afterEach(() => {
|
||||
delete (Object.prototype as Record<string, unknown>).polluted;
|
||||
});
|
||||
|
||||
it("ignores __proto__ keys without polluting Object.prototype", () => {
|
||||
const target = { safe: { keep: true } } as Record<string, unknown>;
|
||||
const source = JSON.parse('{"safe":{"next":1},"__proto__":{"polluted":true}}') as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
mergeMissing(target, source);
|
||||
|
||||
expect((target.safe as Record<string, unknown>).keep).toBe(true);
|
||||
expect((target.safe as Record<string, unknown>).next).toBe(1);
|
||||
expect(target.polluted).toBeUndefined();
|
||||
expect((Object.prototype as Record<string, unknown>).polluted).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ export type LegacyConfigMigration = {
|
||||
|
||||
import { isSafeExecutableValue } from "../infra/exec-safety.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { isBlockedObjectKey } from "./prototype-keys.js";
|
||||
export { isRecord };
|
||||
|
||||
export const getRecord = (value: unknown): Record<string, unknown> | null =>
|
||||
@@ -32,7 +33,7 @@ export const ensureRecord = (
|
||||
|
||||
export const mergeMissing = (target: Record<string, unknown>, source: Record<string, unknown>) => {
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
if (value === undefined) {
|
||||
if (value === undefined || isBlockedObjectKey(key)) {
|
||||
continue;
|
||||
}
|
||||
const existing = target[key];
|
||||
|
||||
@@ -9,6 +9,7 @@ describe("applyMergePatch prototype pollution guard", () => {
|
||||
expect(result.b).toBe(2);
|
||||
expect(result.a).toBe(1);
|
||||
expect(Object.prototype.hasOwnProperty.call(result, "__proto__")).toBe(false);
|
||||
expect(result.polluted).toBeUndefined();
|
||||
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -35,6 +36,7 @@ describe("applyMergePatch prototype pollution guard", () => {
|
||||
expect(result.nested.y).toBe(2);
|
||||
expect(result.nested.x).toBe(1);
|
||||
expect(Object.prototype.hasOwnProperty.call(result.nested, "__proto__")).toBe(false);
|
||||
expect(result.nested.polluted).toBeUndefined();
|
||||
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { isPlainObject } from "../utils.js";
|
||||
import { isBlockedObjectKey } from "./prototype-keys.js";
|
||||
|
||||
type PlainObject = Record<string, unknown>;
|
||||
|
||||
/** Keys that must never be merged to prevent prototype-pollution attacks. */
|
||||
const BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
||||
|
||||
type MergePatchOptions = {
|
||||
mergeObjectArraysById?: boolean;
|
||||
};
|
||||
@@ -73,7 +71,7 @@ export function applyMergePatch(
|
||||
const result: PlainObject = isPlainObject(base) ? { ...base } : {};
|
||||
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
if (BLOCKED_KEYS.has(key)) {
|
||||
if (isBlockedObjectKey(key)) {
|
||||
continue;
|
||||
}
|
||||
if (value === null) {
|
||||
|
||||
Reference in New Issue
Block a user