A QA sweep found unescaped interpolations outside the SSO work. Auditing them properly
turned up 34 genuine HTML sinks; 23 carry data a user, a device or an identity provider
controls, and those are escaped here.
The ones that mattered:
- app.js renders `user.name` in the shell on EVERY page, and an identity provider's
`name` claim is stored verbatim, so an IdP could script the whole dashboard
- designer element `label`/`location` and widget `location`/`query` land inside
value="" attributes, where a single quote breaks out
- content `folder` lands in a data-folder="" attribute
- device `name` is set by the operator OR reported by the panel itself
- workspace-members renders a SERVER error string through t(), which interpolates raw
⚠️ My first attempt was a codemod over everything my scanner flagged, and it was wrong.
It wrapped `progressText.textContent`, `block.title` and `confirm(...)` — none of which
are HTML, so escaping there shows users literal `<`. Worse, it wrapped
`title: ev.title ? ... : null`, an API PAYLOAD, which would have written escaped markup
into the database. I reverted the whole thing and narrowed to interpolations that are
genuinely inside an HTML template, then read all 34 and chose 23.
Skipped deliberately: static app strings, i18n output, ternaries yielding `selected`,
`window.location.origin`, and sites already escaped.
Verified in Chrome, not by inspection: the payload was seeded into user.name,
device.name, content.filename/folder, widget.name/config and video_wall.name (the first
attempt's seeds silently failed on column names — the API responses are checked now),
then eleven views were loaded. Zero executions, zero live img tags — AND the payload is
visible as inert text in 6/6 views, which is what proves the views rendered it rather
than the test proving nothing.
1609 tests; every frontend module parses as an ES module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bvjey4FNam49MN7ybjcq6A
Each of these views carries its own copy of a fetch helper ending in `.then(r => r.json())`. A 403,
404 or 500 body resolves as an ordinary value, so the surrounding try/catch is unreachable and every
handler treats the failure as success. The shared client in api.js has always thrown on !res.ok;
these local copies never did.
Two concrete consequences, both of which tell the operator something untrue:
- The layout editor renders a Delete button on built-in templates for everyone. The server returns
403. The handler shows "Layout deleted" and re-renders the list with the template still sitting
there.
- A rejected platform-role change in Admin shows "Role updated", and the revert that would put the
dropdown back lives only in the dead catch — so the UI keeps displaying a value the server
refused. The same control in Settings uses the throwing client, so the two pages disagree about
whether the change happened.
All eight now match the shared contract: reject on !ok with the server's own message, and treat 401
as session expiry the way api.js does.
This makes previously-silent failures visible, which is the point — some of them will surface
refusals that were always happening. The layout template Delete button, for instance, is now
honestly reported as refused rather than falsely confirmed; whether that button should be shown at
all is a separate question.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
- Wrap wide tables (admin, settings, reports) in .table-wrap with
min-width on the table so they scroll horizontally on narrow screens
instead of collapsing rows.
- Add global .table-wrap { overflow-x: auto } utility.
- Mobile: add mask-image fade on .tabs right edge to hint scrollability
when tabs overflow; flex-shrink:0 on .tab keeps labels intact.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ScreenTinker - open source digital signage management software.
MIT License, all features included, no license gates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>