Summary
The confirm_btn macro in src/opal/web/templates/opalkit/_macros.html (~line 344) interpolates {{ message }}, {{ action }}, {{ method }}, and {{ redirect }} directly into single-quoted JS strings inside a double-quoted onclick attribute:
onclick="if(confirm('{{ message }}')) { ... fetch('{{ action }}', {method: '{{ method }}', ...}) ... }"
This is the same class of bug as #47 (fixed in #54): Jinja autoescaping does not protect JS-in-attribute contexts, because the HTML parser decodes ' back to ' before the JS is parsed. Any value containing a single quote breaks out of the JS string.
Why this is low priority, not a live vulnerability
Every current caller passes a literal template string or non-user-controlled data:
parts/detail.html:39 — part.internal_pn (system-generated, no quotes)
risks/detail.html:197, datasets/detail.html:56 — static literals
So there is no exploitable path today. The risk is latent: the first caller that interpolates a user-controlled name (e.g. message="Delete " ~ part.name ~ "?") silently introduces a stored XSS, with nothing at the macro to stop it.
Suggested fix
Route the interpolated values through | tojson, following the convention established in #54 (single-quoted onclick attribute + {{ value | tojson }}). Note the macro currently uses a double-quoted attribute with many nested single-quoted JS strings, so the quoting needs restructuring — either switch the attribute to single quotes (and tojson the literal-quote values) or use | tojson | forceescape to keep it usable inside the double-quoted attribute.
Add a rendering test alongside tests/unit/test_template_xss.py that renders a confirm_btn whose message contains ' and \ and asserts the confirm() argument JSON-round-trips.
Found while fixing #47.
Summary
The
confirm_btnmacro insrc/opal/web/templates/opalkit/_macros.html(~line 344) interpolates{{ message }},{{ action }},{{ method }}, and{{ redirect }}directly into single-quoted JS strings inside a double-quotedonclickattribute:onclick="if(confirm('{{ message }}')) { ... fetch('{{ action }}', {method: '{{ method }}', ...}) ... }"This is the same class of bug as #47 (fixed in #54): Jinja autoescaping does not protect JS-in-attribute contexts, because the HTML parser decodes
'back to'before the JS is parsed. Any value containing a single quote breaks out of the JS string.Why this is low priority, not a live vulnerability
Every current caller passes a literal template string or non-user-controlled data:
parts/detail.html:39—part.internal_pn(system-generated, no quotes)risks/detail.html:197,datasets/detail.html:56— static literalsSo there is no exploitable path today. The risk is latent: the first caller that interpolates a user-controlled name (e.g.
message="Delete " ~ part.name ~ "?") silently introduces a stored XSS, with nothing at the macro to stop it.Suggested fix
Route the interpolated values through
| tojson, following the convention established in #54 (single-quotedonclickattribute +{{ value | tojson }}). Note the macro currently uses a double-quoted attribute with many nested single-quoted JS strings, so the quoting needs restructuring — either switch the attribute to single quotes (andtojsonthe literal-quote values) or use| tojson | forceescapeto keep it usable inside the double-quoted attribute.Add a rendering test alongside
tests/unit/test_template_xss.pythat renders aconfirm_btnwhosemessagecontains'and\and asserts theconfirm()argument JSON-round-trips.Found while fixing #47.