What
Three more plain-object maps are built by assigning a user-authored string key directly (out[key] = …), which is the same defect #551 fixed for Dashboard placement maps. A key of __proto__ invokes the inherited Object.prototype setter and creates no own property, so the entry silently vanishes; a bare read of the same key resolves up the prototype chain to Object.prototype itself, which a merge-then-rewrite can then mutate realm-wide.
Sites
src/dashboard/model/dashboard-variable-store.ts:66,76,93,96 — out[variableId], out[dashboardId]
src/workspace/workspace-dashboards.ts:133 — configs[name] = config
src/dashboard/application/dashboard-viewer-session.ts:1495-1496 — proposedValues[variable.def.parameter]
All three are keyed by strings the user authors: variable IDs, dashboard IDs, and variable parameter names (the {name:Type} placeholder text in panel SQL). Nothing constrains those away from __proto__ or constructor.
Why this is cheap to fix
#551 already landed the two shared primitives in src/core/saved-query.ts and exported them:
defineJsonField(target, key, value) — Object.defineProperty-based write
readJsonField(target, key) — own-property-only read, for any map that gets merged, mutated, or where "no entry" differs from "an empty entry"
So this is a mechanical sweep plus round-trip tests with __proto__/constructor keys that assert the entry survives (Object.hasOwn + exact value), not merely that nothing throws.
Why deferred
Out of scope for #551, which was explicitly about placement maps keyed by tile ID — one sweep across every site it named, rather than quietly widening that PR's diff. Found while doing that sweep (PR #558).
Note on severity
#551's body originally claimed "no prototype pollution escapes". That is not true of the read path: setStylePlacement was reproduced polluting Object.prototype.grid for the whole realm. Whether any of the three sites above is reachable that way has not been checked — each needs the read side examined, not just the write.
What
Three more plain-object maps are built by assigning a user-authored string key directly (
out[key] = …), which is the same defect #551 fixed for Dashboard placement maps. A key of__proto__invokes the inheritedObject.prototypesetter and creates no own property, so the entry silently vanishes; a bare read of the same key resolves up the prototype chain toObject.prototypeitself, which a merge-then-rewrite can then mutate realm-wide.Sites
src/dashboard/model/dashboard-variable-store.ts:66,76,93,96—out[variableId],out[dashboardId]src/workspace/workspace-dashboards.ts:133—configs[name] = configsrc/dashboard/application/dashboard-viewer-session.ts:1495-1496—proposedValues[variable.def.parameter]All three are keyed by strings the user authors: variable IDs, dashboard IDs, and variable parameter names (the
{name:Type}placeholder text in panel SQL). Nothing constrains those away from__proto__orconstructor.Why this is cheap to fix
#551 already landed the two shared primitives in
src/core/saved-query.tsand exported them:defineJsonField(target, key, value)—Object.defineProperty-based writereadJsonField(target, key)— own-property-only read, for any map that gets merged, mutated, or where "no entry" differs from "an empty entry"So this is a mechanical sweep plus round-trip tests with
__proto__/constructorkeys that assert the entry survives (Object.hasOwn+ exact value), not merely that nothing throws.Why deferred
Out of scope for #551, which was explicitly about placement maps keyed by tile ID — one sweep across every site it named, rather than quietly widening that PR's diff. Found while doing that sweep (PR #558).
Note on severity
#551's body originally claimed "no prototype pollution escapes". That is not true of the read path:
setStylePlacementwas reproduced pollutingObject.prototype.gridfor the whole realm. Whether any of the three sites above is reachable that way has not been checked — each needs the read side examined, not just the write.