fix(#551): keep __proto__/constructor tile IDs' placements, and stop a real Object.prototype leak - #558
Merged
Merged
Conversation
…fineJsonField/readJsonField Every Dashboard placement-map write (grafana-grid-layout.ts, flow-layout.ts, dashboard-document.ts's legacy migrations/fallback regeneration, dashboard-commands.ts's duplicate-tile and flow->grid change-layout) now writes through core/saved-query.ts's defineJsonField (Object.defineProperty) instead of a bare map[tileId] = value, so a tile id of '__proto__' or 'constructor' (both schema-legal) survives as an own property instead of silently invoking the inherited Object.prototype setter. Self-review surfaced a second, more severe instance of the same root cause: setStylePlacement's merge-then-rewrite read a not-yet-owned entry via a bare items[tileId], which for '__proto__' resolves to Object.prototype itself, then mutated it in place -- real prototype pollution, not just a dropped placement. Every read that could be merged/mutated now goes through a new paired readJsonField helper (own-property-only). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiubaoqEuBzAyo5C4P8Vqr
This was referenced Jul 29, 2026
Closed
lesandie
pushed a commit
to lesandie/altinity-sql-browser
that referenced
this pull request
Aug 2, 2026
The e2e job was gated to tags, schedule and manual dispatch, so it reported `skipped` on every pull request and every push to `main` while the overall run still reported success. A check that never runs is not a check. The cost was concrete: two `tile-open-workbench` specs have been failing deterministically on chromium, firefox AND webkit, and nothing surfaced it. Today's `bug`/`low` batch (Altinity#554-Altinity#558, Altinity#561-Altinity#563) all merged "green"; two of those PRs changed or added e2e specs that CI never executed. The failures only appeared when the workflow was dispatched by hand (6 failed / 568 passed on `main`), and a second dispatch at f68861c showed the identical 6 failures, so they pre-date that batch. Filed as Altinity#565. Pull requests now run **Chromium only**, path-gated on anything that can move rendered geometry (`src/**` — which is where `styles.css` lives — plus `tests/e2e/**`, `playwright.config.js`, `build/**`, `schemas/**`, the manifests and the workflows). Both Altinity#565 failures reproduce on Chromium, so one engine is enough to have caught them, and it is cheap enough to sit on every relevant PR. Tags, nightly and manual dispatch keep the full three-engine matrix: engine-specific breakage is real here, and that is what a release has to clear. The `gate` job needed no change — it already fails on any `failure` among its needs and tolerates a legitimately skipped job. Altinity#565's two specs are quarantined with `test.fail()` so this gate is meaningful from the first run rather than red on arrival. Deliberately not `skip`/`fixme`: the specs keep executing, so when the underlying bug is fixed they report "expected to fail, but passed" and the quarantine has to be removed. Their assertions encode Altinity#535's intended widen semantics and must not be rewritten to match the broken output. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiubaoqEuBzAyo5C4P8Vqr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Closes #551. The Dashboard schema permits any non-whitespace tile ID (
dashboardTileV1.id,pattern: "\\S"), so__proto__andconstructorare legal tile IDs — andJSON.parsecreates__proto__as an own property, which is how such an ID reaches the app. But every tile-keyed placement map was built with a bareitems[tileId] = placement, which for'__proto__'invokes the inheritedObject.prototypesetter and creates no own property: the placement silently vanished and the tile fell back to the engine default size.defineJsonFieldinsrc/core/saved-query.tsalready usedObject.definePropertyfor exactly this input class. It is now exported as the one shared write primitive and used at every placement-map write acrosssrc/dashboard/layouts/andsrc/dashboard/model/. No baremap[tileId] = …remains in those layers.The read side was worse than the issue described
The issue stated "No prototype pollution escapes: only the local map's
[[Prototype]]changes, neverObject.prototype." That turned out to be wrong, and a test added during self-review caught it.A bare read —
items[tileId]wheretileId === '__proto__'and the map has no own property under that name — resolves up the prototype chain toObject.prototypeitself. That's a real object, so anisObject(...)guard cannot tell it apart from genuine stored data.setStylePlacement's merge-then-rewrite (current[style] = next, preservingcurrent's other fields) therefore assigned directly onto the aliasedObject.prototype, pollutingObject.prototype.gridfor every other placement map in the realm.This was reproduced live and confirmed by sabotage, not reasoned about hypothetically. A paired
readJsonField(own-property-only read) now guards every read of a caller-keyed JSON map that can be merged, mutated, or that distinguishes "no entry" from "an empty one".I've corrected that claim on the issue body so the record isn't wrong.
Coverage
Round-trips tested with tile IDs
__proto__andconstructorthrough:grafana-grid@1→@2migration;flow@1→@2(both report and grid presets); v2 fallback regeneration;update-placement;duplicate-tile(flow, grid@1 and the grid@2 style-map branch); and encode→decode of both the stored workspace and a portable bundle. Every assertion checksObject.hasOwnplus the exact surviving span/height andJSON.stringifyround trip — not merely that nothing throws, which is the unfalsifiable shape the issue warned about.Sabotage-verified both fixes: reverting
setGridPlacementto a bare assignment turns the new test red; revertingsetStylePlacement's read to the bare aliasing form reproduces literalObject.prototype.gridpollution, caught by the pre-existingderiveFlowFallbackschema-validation test.Follow-up filed
The same bug class exists at three sites outside this issue's placement-map scope (variable/dashboard ID and variable-parameter-name keyed maps in
dashboard-variable-store.ts,workspace-dashboards.tsanddashboard-viewer-session.ts). Left untouched here and filed separately asinboxrather than folded into this PR.Checklist
npm testpasses (the per-file coverage gate is non-negotiable)npm run buildsucceeds (single-filedist/sql.html)src/core/, network insrc/net/(injected fetch), DOM insrc/ui/CHANGELOG.md([Unreleased]) updated if behavior or the deployed surface changed🤖 Generated with Claude Code
https://claude.ai/code/session_01GiubaoqEuBzAyo5C4P8Vqr