fix(ui): dashboard error modal dead — inline onclick broke on apostrophes #17
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/inline-onclick-injection"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The reported bug
Clicking a failed backup pill on the dashboard did nothing. Console:
Root cause
The pill built its click handler as inline JavaScript source inside a single-quoted HTML attribute:
JSON.stringifyescapes"and\— but not'. An apostrophe in the error closes the attribute early, so the browser compiles a truncated fragment:The handler never binds, so the click is a no-op. Latent until
02d25c5started folding raw command stderr into failure messages —pg_dumpoutput is full ofcouldn't/doesn't.The same hole was an XSS vector:
<script>in an error message rendered as live markup.The fix
Data never becomes code.
escapeAttr()— the existingescapeHtml()goes throughinnerHTML, which leaves quotes and apostrophes untouched, so it was never safe for attribute values. That misuse was the shared root of every site below.delegateClicks(handlers)— binds bydata-action. All 26 call sites acrossdashboard/nodes/stack_backups/users/backupsnow carry arguments in escapeddata-*attributes.Delegation dispatches only the innermost
[data-action], which removes the threeevent.stopPropagation()calls the node cards needed for nested buttons.Two bugs found while sweeping (worse than the reported one)
backups.jsdownload/restore/delete interpolated filenames with no escaping at all — notescapeHtml, nothing.value="${escapeHtml(item.path)}"— these values decide which paths get restored. A path containing"silently truncated the value, so you'd restore a different set than the one checked. That's data corruption in a restore path, not a dead button.Also fixed unescaped error text in both volume-contents failure paths, and three attribute-context
escapeHtml→escapeAttrindashboard.js.Verification
SyntaxErrorbefore any fix.data-actionsite and its emitted attributes, parsed eachdelegateClicksblock for the dataset properties its handlers read, and compared. This catches the failure mode a sweep this size actually hits (data-node-namevsd.nodeName). All 26 sites consistent; no orphan handlers, no unhandled actions.node-o'brien "prod" <b>,jim's_db "2026".sql.gz, stderr with quotes): 25 checks, all passing. Values round-trip byte-exact, no markup injection, button-inside-card fires only the button, card body still opens detail, disabled controls inert.No Ruby changed, so RSpec is unaffected — I did not run it, and it proves nothing about this change either way.
Note for deploy
/js/is served via Sinatraenable :staticwith no cache-busting. A browser holding a cachedcommon.jswould pair old JS with new markup and the buttons would be dead again. Worth a hard refresh when verifying in prod; cache-busted asset URLs would be a reasonable follow-up.Left alone deliberately
Six
onclickhandlers remain — constant source (this.parentElement.remove(),closeModal()) or integer arithmetic (pagination). No data flows into any of them, so converting them would be churn without risk reduction.