Aaron Elijah Mars created WW-5772:
-------------------------------------
Summary: Remove framework-generated inline event handlers from
transfer-select, updownselect and combobox tags (CSP)
Key: WW-5772
URL: https://issues.apache.org/jira/browse/WW-5772
Project: Struts 2
Issue Type: Improvement
Components: Core Tags
Reporter: Aaron Elijah Mars
h3. Problem
The {{optiontransferselect}}, {{inputtransferselect}} and {{updownselect}} tags
render every button with an {{onclick}} handler that the framework builds
itself. The {{combobox}} tag renders its select with an inline {{onChange}}. An
application that wants a nonce-based Content-Security-Policy without
{{'unsafe-inline'}} therefore cannot use these tags, even though {{<s:script>}}
already applies the nonce to the scripts the same templates emit.
The generated {{onclick}} code also contains the
{{headerKey}}/{{doubleHeaderKey}} values as JavaScript string literals that are
only HTML-escaped. The browser decodes {{'}} back to {{'}} before it runs
the handler, so a header key that contains a quote breaks the handler.
h3. Approach
- *Transfer selects and updownselect.* Each button now carries its action, the
target select id(s) and the header key(s) in {{data-struts-*}} attributes,
which FreeMarker HTML-escapes. {{optiontransferselect.js}} (also used by
updownselect) and {{inputtransferselect.js}} each register one delegated click
listener per page, in the capture phase, guarded against double registration.
The listener calls the same existing functions with the same arguments as the
old inline handlers, header-key/no-header-key branches included.
{{optiontransferselect.ftl}} folds its duplicated per-button branches into one
{{transferButton}} macro, about 100 lines shorter. A developer-supplied
{{*Onclick}} attribute is still rendered as {{onclick}} and still runs after
the framework action.
- *Combobox (simple and html5).* The select carries
{{data-struts-combobox="<escapedId>"}}. The {{change}} listener is attached
from an {{<@s.script>}} block, which carries the nonce like the
{{autoPopulate_<id>}} function the template already emits. The script binds
every select with that marker that is not bound yet. Repeated comboboxes with
the same id (e.g. inside an iterator) therefore each get exactly one handler,
and fragments loaded over AJAX work too: jQuery {{.html()}} evaluates scripts
through a temporary element in {{<head>}}, so {{document.currentScript}} cannot
be used.
- The old global JS functions are unchanged, so custom themes that copied the
old templates keep working.
h3. Why it is systematic
Each of these tags had the same pattern: framework-built JavaScript in
event-handler attributes. The patch removes that pattern from them entirely
rather than escaping individual values, so neither the CSP blocker nor the
quoting problem can come back through these templates.
h3. Upgrade note (please add to the release notes / migration guide)
The new markup needs the new {{optiontransferselect.js}} and
{{inputtransferselect.js}}.
- An application that serves its own copy of the static files
({{struts.serve.static=false}} or a custom {{struts.ui.staticContentPath}})
must update those two files together with the templates. Otherwise the transfer
buttons silently do nothing.
- The static URL is not versioned and browsers cache it for one day, so a
browser holding the old copy right after an upgrade sees inactive buttons until
the cache expires.
- Code that calls {{button.onclick()}} directly no longer triggers the
framework action. Real clicks and {{element.click()}} still do.
h3. Follow-ups (separate tickets; this PR does not make the themes fully
CSP-clean)
- xhtml/css_xhtml {{form.ftl}}: default {{onreset="clearErrorMessages..."}} and
the {{onsubmit="return validateForm_..."}} for client-side validation
- xhtml {{tooltip.ftl}}: {{domTT_activate}} {{onmouseover}} when
{{javascriptTooltip}} is on
- {{doubleselect}}: the {{onchange="<id>Redirect(...)"}} built in
{{DoubleSelect.java}}
This change was prepared with AI assistance and reviewed and tested by me.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)