harden: drop prototype-pollution keys in configPatch merge

Skip `__proto__`, `prototype`, and `constructor` keys while recursively
merging provider-auth `configPatch` payloads. Plugins construct the
patch in-process today, but JSON-parsed sources can preserve these keys
and the assignment `next[key] = value` would otherwise mutate the
merge target's prototype chain.

Made-with: Cursor
This commit is contained in:
Neerav Makwana
2026-04-22 22:20:54 -04:00
committed by Peter Steinberger
parent 14d1c9c4f0
commit a849283f80
2 changed files with 15 additions and 0 deletions

View File

@@ -42,6 +42,14 @@ describe("applyProviderAuthConfigPatch", () => {
expect(next.agents?.defaults?.model).toEqual(base.agents.defaults.model);
});
it("drops prototype-pollution keys from the merge", () => {
const patch = JSON.parse('{"__proto__":{"polluted":true},"agents":{"defaults":{}}}');
const next = applyProviderAuthConfigPatch(base, patch);
expect(next.agents?.defaults?.models).toEqual(base.agents.defaults.models);
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
expect(Object.getPrototypeOf(next).polluted).toBeUndefined();
});
it("keeps normal recursive merges for unrelated provider auth patch fields", () => {
const base = {
agents: {

View File

@@ -46,6 +46,10 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
// Guard the recursive merge against prototype-pollution payloads if a
// patch ever arrives from a JSON-parsed source that preserves these keys.
const BLOCKED_MERGE_KEYS = new Set(["__proto__", "prototype", "constructor"]);
export function mergeConfigPatch<T>(base: T, patch: unknown): T {
if (!isPlainRecord(base) || !isPlainRecord(patch)) {
return patch as T;
@@ -53,6 +57,9 @@ export function mergeConfigPatch<T>(base: T, patch: unknown): T {
const next: Record<string, unknown> = { ...base };
for (const [key, value] of Object.entries(patch)) {
if (BLOCKED_MERGE_KEYS.has(key)) {
continue;
}
const existing = next[key];
if (isPlainRecord(existing) && isPlainRecord(value)) {
next[key] = mergeConfigPatch(existing, value);