Summary
The BOM table's EDIT affordance in src/opal/web/templates/parts/_section_stock.html (~line 36) builds an inline JS call with only single quotes escaped:
onclick="editBomLine({{ line.id }}, {{ line.quantity }}, '{{ (line.reference_designator or '') | replace("'", "\\'") }}', '{{ (line.notes or '') | replace("'", "\\'") }}'); return false;"
Jinja autoescape protects the HTML attribute but not the JS string literal inside it. The receiving function is editBomLine(lineId, qty, refdes, notes) in src/opal/web/templates/parts/detail.html (~line 491).
Impact
All reachable via the plain-text NOTES/REFDES inputs or the JSON API / MCP add_component:
- A note ending in a backslash renders
'...\' — the backslash escapes the closing JS quote, the handler is a SyntaxError, and the EDIT button is dead for that row.
- Mid-string backslashes silently corrupt (
C:\temp becomes C:<tab>emp).
- A literal newline (settable via API) produces an unterminated JS string literal.
- It is injectable: notes of
\');alert(1);// pass the replace as \\');alert(1);// — literal backslash, real closing quote — and the rest executes as JS when EDIT is clicked. Stored XSS for any authenticated user who can edit BOM notes.
Fix
Replace the manual quoting with Jinja's tojson, which is attribute-safe under autoescape and handles quotes, backslashes, and newlines:
onclick="editBomLine({{ line.id }}, {{ line.quantity }}, {{ (line.reference_designator or '') | tojson }}, {{ (line.notes or '') | tojson }}); return false;"
Also:
- Scan the parts templates for the same
replace("'", "\\'") pattern and convert any other inline-JS argument passing to tojson as well.
- Add a unit test rendering the part detail page with a BOM line whose notes contain a trailing backslash, a single quote, and a newline, asserting the rendered
onclick stays syntactically valid (e.g. contains the tojson-escaped form, no bare backslash-quote).
- Verify with
uv run pytest --no-cov and uv run ruff check src/.
Summary
The BOM table's EDIT affordance in
src/opal/web/templates/parts/_section_stock.html(~line 36) builds an inline JS call with only single quotes escaped:onclick="editBomLine({{ line.id }}, {{ line.quantity }}, '{{ (line.reference_designator or '') | replace("'", "\\'") }}', '{{ (line.notes or '') | replace("'", "\\'") }}'); return false;"Jinja autoescape protects the HTML attribute but not the JS string literal inside it. The receiving function is
editBomLine(lineId, qty, refdes, notes)insrc/opal/web/templates/parts/detail.html(~line 491).Impact
All reachable via the plain-text NOTES/REFDES inputs or the JSON API / MCP
add_component:'...\'— the backslash escapes the closing JS quote, the handler is aSyntaxError, and the EDIT button is dead for that row.C:\tempbecomesC:<tab>emp).\');alert(1);//pass the replace as\\');alert(1);//— literal backslash, real closing quote — and the rest executes as JS when EDIT is clicked. Stored XSS for any authenticated user who can edit BOM notes.Fix
Replace the manual quoting with Jinja's
tojson, which is attribute-safe under autoescape and handles quotes, backslashes, and newlines:onclick="editBomLine({{ line.id }}, {{ line.quantity }}, {{ (line.reference_designator or '') | tojson }}, {{ (line.notes or '') | tojson }}); return false;"Also:
replace("'", "\\'")pattern and convert any other inline-JS argument passing totojsonas well.onclickstays syntactically valid (e.g. contains the tojson-escaped form, no bare backslash-quote).uv run pytest --no-covanduv run ruff check src/.