Round-6 review fixes: untrusted-diagram attack surface, multi-tab data loss, animation bounds - #66
Merged
Merged
Conversation
Round-6 findings 1, 3 and 4, plus the formula code-execution sink I confirmed before the review ran and the display-font beacon it flagged as a lead. A diagram is untrusted input: it arrives as a shared #d= link, a downloaded .json or .econ, a library component, or a cli.js argument. Four ways it could act on the reader. 1. evalFormula fell through to new Function(expr) whenever math.js could not parse an expression, and validateFormula reported such a payload as valid so nothing warned. reset() evaluates register formulas on load, so opening a share link ran the author's JavaScript with no click at all, and running "node cli.js someones-economy.econ" ran it with the invoking user's privileges. The fallback is gone. Nothing needs it: all 110 distinct expressions across the 12 demos and 11 templates compile under math.js, and the JS-flavoured spellings a saved diagram might use are handled on the math.js path instead, by exposing a seeded Math object in scope and by retrying an expression math.js has already refused with the operators rewritten (&& to and, || to or, ! to not, ** to ^, === to ==). One deliberate semantic change: "a && b" used to return b's value under JS truthiness and now returns 1 or 0. These are numeric fields, so a boolean is the more useful answer, and it is the same answer math.js gives everywhere else. 2. Parameter names were interpolated into the sweep results table with innerHTML and no escaping, so a parameter named with an img onerror ran the author's script in the app's origin the moment the reader pressed Run sweep, with read access to sim_library and sim_autosave. Every other interpolation in that function already escaped; this one did not. Checked the rest of the app's innerHTML sinks: this was the only unescaped one carrying diagram text. 3. meta.name was interpolated into the header banner of the generated module, so a name containing the comment terminator ended it early and the rest became top-level code that ran on require(). The banner escapes it now, and the module's global name is held to an identifier. 4. meta.font was pasted into a fonts.googleapis.com stylesheet URL, so opening a shared diagram fired a request to a third party carrying a string of the sender's choosing, telling them the reader's IP and that they had opened it. It also contradicted the no-Google-Fonts-at-runtime claim in CLAUDE.md. Only a family from the curated list is honoured now. Separately, opening a share link replaced the reader's autosaved diagram with no prompt, and a page load has no undo stack for state from before it, so the work was simply gone. The link is still adopted (a reload has to restore what is on screen), but it asks first when there is real work in the saved slot, and puts the reader's diagram back if they decline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
…ted-node placeholder Round-6 findings 6 and 7. Finding 6 is a regression from the previous round. At step 0 the amount field is labelled Starting amount, but the +/- buttons beside it went through addResources/takeResources, which touch only the live count; setCount is what writes the reset baseline. So three clicks on "+" changed the canvas, the properties readout, the undo stack and the autosaved file, and then Reset or a page reload silently put the old number back. The autosave recorded the contradiction directly: resources 13 next to initialResources 10. A nudge at rest now moves the baseline with the count, while still preserving a mixed holding's colours, which is why it does not just call setCount. The "(node deleted)" placeholder added last round was a real selectable option with an empty value. Choosing it cleared the rule's target, and the next render fell back to displaying the first interactive node with no warning at all, which is the exact misleading state the placeholder was added to prevent. It is disabled now: shown as the current value, impossible to pick. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
Round-6 findings 13 and 14. Library Delete, Duplicate and Rename, and the components Delete, all wrote back the array the list had been rendered from rather than what storage held at the moment of the edit. So a diagram another tab saved in between was erased along with the edit: measured through the real Delete menu item, a library holding one entry from each of two tabs ended up empty. Those edits now re-read storage first and locate their target by a stable per-entry id, falling back to a content match for rows saved before ids existed. The save paths already read fresh, and now stamp an id. sim_autosave is a single slot shared by every tab on the origin, so the tab that saves last silently becomes the saved copy while the other carries on believing its work is safe. Nothing can merge two diagrams, but the superseded tab can be told, once, while its work is still on screen and can be exported. A storage listener does that; the event fires only in other tabs, so a write never warns the tab that made it, and embeds stay silent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
…ee panels lying Round-6 findings 2, 5, 8, 9, 10, 11 and 12. Drawing a gate's weight label mirrors the engine and evaluates the formula, and a weight formula can draw random numbers. The canvas repaints on hover, pan, zoom, selection and every step, so painting was consuming draws the run was about to make: the same diagram under the same seed produced different results depending on how much the user moved the mouse. Measured directly: a single render moved the seeded stream position. Label evaluation now saves and restores it, the way detectLoops already guards its probes. The animation layer had no ceiling and no idea when it was off screen. Cleanup runs from requestAnimationFrame, which a browser suspends in a hidden tab while the setInterval driving the steps keeps firing, so a run left in another tab produced dots that nothing consumed: measured growing to 2431 live elements. Even in the foreground a heavy model at the top of the speed slider held 838 at once, each costing a getPointAtLength per frame, which is what dragged the page to about 17fps and made the run fall behind the speed the user asked for. There is now a hard ceiling of 240 live dots, spawning is skipped entirely while the document is hidden, and a visibilitychange drops what is in flight and repaints on return. Hiding the overflow menu in embeds last round left a narrow embed with no way to reset a run at all: the small-screen rules hide Reset from the topbar on the assumption that the menu still carries it. An embed offers Run, Step and Reset, so Reset comes back at those widths. Loading a starter template left the Simulation panel showing the empty canvas it had been drawn against: a blank Name and a FILE block reading 0 nodes and 0 connections beside a canvas full of them. _installTemplate repaints the panel. Food Web claimed "Ten species lock into coupled, bounded oscillations", but the Foxes crash to 1 and stay there for the rest of the run while foxBirths sits at 0. This is not a mistuned constant: at three times the birth rate and a quarter of the death rate they still peak at 167 and collapse to 1, because their break-even prey population is near the rabbit peak and they eat the base out from under themselves. That is a real dynamic of this coupling, so the mechanics are untouched and the description now says what the model does. Retuning the predator-prey coupling to make foxes persist is a design decision about the demo rather than a defect fix, and the surrounding web is sensitive to it: pushing fox efficiency collapsed the rabbit oscillation from a span of 93 to 17. The smoke suite gained a standing check that every bundled template runs 120 steps with every value finite, since it previously only asserted that they load. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
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.
Fixes all fourteen findings from the sixth multi-agent review, plus the formula code-execution sink I confirmed before launching it.
Each fix has a test that I verified fails against the unpatched code first. 258 unit tests, smoke green.
Security: a diagram is untrusted input
The product invites exactly this. It advertises "Copy share link" (a base64 diagram in the
#d=hash), File > Open of.jsonand.econ, library components, andnode cli.js someones-economy.econ. Five ways a diagram could act on the reader:evalFormulafell through tonew Function(expr)validateFormulareports it invalidinnerHTML<img>elements, script running in the app's origin with read access tosim_libraryandsim_autosavemeta.nameinterpolated into the generated module's banner commentrequire()meta.fontpasted into afonts.googleapis.comURLRemoving the formula fallback needed evidence it was safe. All 110 distinct expressions across the 12 demos and 11 templates compile under math.js. The JS-flavoured spellings a saved diagram might use still work: a seeded
Mathobject is exposed in scope, and an expression math.js refuses is retried with the operators rewritten (&&→and,||→or,!→not,**→^,===→==). One deliberate semantic change:a && bused to returnb's value under JS truthiness and now returns 1 or 0, which is the answer math.js gives everywhere else in these numeric fields.The
meta.fontleak also contradicted the no-Google-Fonts-at-runtime claim inCLAUDE.mdandREADME.md.Data loss
sim_autosaveis one slot shared by every tab. Nothing can merge two diagrams, but the superseded tab is now told once, while its work is still on screen and exportable.resources: 13next toinitialResources: 10.Determinism and animation
render()moved the stream position.requestAnimationFrame, which browsers suspend in a hidden tab while thesetIntervaldriving steps keeps firing. Measured: 838 live dots in the foreground on a heavy model at top speed, growing to 2431 while hidden. Now capped at 240, skipped entirely whiledocument.hidden, and dropped onvisibilitychange.Panels that lied
(node deleted)placeholder added last round was selectable, and picking it restored the exact misleading state it exists to prevent. It is disabled now.Food Web: description corrected, mechanics untouched
The template claimed "Ten species lock into coupled, bounded oscillations", but Foxes crash to 1 and stay there with
foxBirthsat 0. This is not a mistuned constant: at three times the birth rate and a quarter of the death rate they still peak at 167 and collapse to 1, because their break-even prey population sits near the rabbit peak and they eat the base out from under themselves.Retuning the predator-prey coupling is a design decision about the demo rather than a defect fix, and the surrounding web is sensitive to it: pushing fox efficiency collapsed the rabbit oscillation from a span of 93 to 17. So the mechanics are unchanged and the description now says what the model does.
Testing
node test/run.js— 258 passed (was 255 on main)npm run smoke— passed, no console or page errorsTwo caveats
document.hiddenguard is not provable in this harness. Headless Chromium reports a backgrounded page asvisible, so the test covers what is verifiable: the 240 cap, and the visibility handler driven through a realvisibilitychangeevent.Generated by Claude Code