Skip to content

Stored XSS / broken EDIT handler: BOM table inline onclick escapes only single quotes #47

Description

@CST-100

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/.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions