screentinker/frontend/js
ScreenTinker ec450929ce Escape user-controlled data at the HTML sinks it actually reaches
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
2026-08-11 11:44:10 -05:00
..
components SSO: build the operator approval screen, and close the last of the QA findings 2026-08-11 08:48:53 -05:00
i18n Close the third QA round: limiter bypass, stored XSS, break-glass, org placement 2026-08-11 11:25:11 -05:00
lib Preview a rotated display the way people see it, not the way its framebuffer is 2026-08-06 09:38:31 -05:00
views Escape user-controlled data at the HTML sinks it actually reaches 2026-08-11 11:44:10 -05:00
agency-portal.js feat(agency): per-token upload folder — auto-created, subtree-confined (#158) (#171) 2026-07-12 21:23:25 -05:00
api.js The settings PIN can be rotated and set from the dashboard 2026-08-05 13:44:15 -05:00
app.js Escape user-controlled data at the HTML sinks it actually reaches 2026-08-11 11:44:10 -05:00
brand-prime.js fix(branding): inject instance branding into the app shell, no default flash (#76) 2026-06-11 09:30:23 -05:00
branding.js fix(branding): no ScreenTinker default flash on load/switch (#38) 2026-06-09 11:43:42 -05:00
i18n.js i18n: register Italian locale in language registry (followup to PR #2) 2026-05-11 20:05:09 -05:00
socket.js Surface the screenshot-request verdict as a toast 2026-08-07 08:52:19 +01:00
utils.js fix(dashboard): make auth-image hydration lazy by default (#182 follow-up) (#185) 2026-07-14 13:28:12 -05:00