fix(#1533): prototype-pollution guard in _deepMergeConfig (#1534)

* fix: prototype-pollution guard in _deepMergeConfig (audit M4)

The root↔workstream config merge iterated Object.keys(overlay) with no
__proto__/constructor/prototype guard, while four sibling paths in the same
file (lines ~315/319/331/341/549) guard them. A workstream/root config.json
with {"__proto__": {...}} could pollute the merged object's prototype chain
and spoof unset config flags (per-object, not global Object.prototype).

Adds the same three-key continue guard at the top of the overlay loop plus a
regression test for __proto__/constructor/prototype overlay keys.

Closes a gap missed by the closed config proto-pollution hardening
(#751/#1406/#663).

Claude-Session: https://claude.ai/code/session_01R88n7Q54bAaVHFkDbbH1yz

* chore(changeset): Fixed fragment for #1534 (config proto-pollution guard)

Claude-Session: https://claude.ai/code/session_01R88n7Q54bAaVHFkDbbH1yz

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
Rezolv
2026-06-21 22:46:34 -04:00
committed by GitHub
parent 8748e95ed1
commit 75552f7ea0
3 changed files with 37 additions and 1 deletions

View File

@@ -138,6 +138,11 @@ function _deepMergeConfig(base: Record<string, unknown>, overlay: Record<string,
if (typeof base !== 'object' || typeof overlay !== 'object') return overlay;
const result: Record<string, unknown> = { ...base };
for (const key of Object.keys(overlay)) {
// Prototype-pollution guard — mirrors the four sibling guards in this file
// (lines ~315/319/331/341/549). Without it a workstream/root config.json with
// {"__proto__": {...}} pollutes this merged object's prototype chain and can
// spoof unset config flags. (Per-object pollution, not global Object.prototype.)
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
if (overlay[key] !== null && typeof overlay[key] === 'object' && !Array.isArray(overlay[key])) {
result[key] = _deepMergeConfig((base[key] ?? {}) as Record<string, unknown>, overlay[key] as Record<string, unknown>);
} else {