Skip to content

Let autocomplete selections and ChoiceFilter auto-submit work under the default CSP (WICKET-6938) - #1667

Open
aeonframework wants to merge 5 commits into
apache:masterfrom
aeonframework:csp-autocomplete-choicefilter
Open

aeonframework wants to merge 5 commits into
apache:masterfrom
aeonframework:csp-autocomplete-choicefilter

Conversation

@aeonframework

@aeonframework aeonframework commented Oct 14, 2026 •

Copy link
Copy Markdown

Resolves WICKET-6938.

Problem

Wicket's default Content Security Policy (strict(): script-src 'strict-dynamic' 'nonce-…') blocks two things wicket-extensions still relies on:

  • AutoCompleteTextField selection hooks. AbstractAutoCompleteRenderer#getOnSelectJavaScriptExpression is written into an onselect attribute of each choice, and wicket-autocomplete.js runs it with eval(). Without 'unsafe-eval' that throws, so selecting an item with an expression does nothing, and the only way to use the hook is to weaken the CSP for the whole application. The hook also invites building JavaScript from item data by string concatenation, as its own javadoc examples do; the markup escaping of the attribute does not protect that data once eval() decodes it.
  • ChoiceFilter auto-submit. With autoSubmit the select is rendered with onchange="this.form.submit();", an inline handler the CSP blocks, so the filter silently stops submitting on change.

Approach

Two commits, each standing on its own:

  1. ChoiceFilter registers the same handler with an OnEventHeaderItem, the way FilterForm#enableFocusTracking already does, so the script carries the CSP nonce. The markup only loses the attribute.
  2. Autocomplete dispatches a cancelable, bubbling wicket-autocomplete-select CustomEvent on the input. detail.item is the chosen element and detail.value the text value; a listener may replace detail.value, or call preventDefault() to leave the input unchanged (what returning null from the expression did). Data a listener needs comes from the new AbstractAutoCompleteRenderer#getChoiceData(T), written as markup-escaped data-* attributes whose names must match [a-z][a-z0-9-]*. The listener is registered from renderHead(), so it carries the nonce and nothing is evaluated. The class javadoc shows a complete listener.

getOnSelectJavaScriptExpression is @Deprecated(since = "10.12.0", forRemoval = true) and keeps working where 'unsafe-eval' is allowed: its result becomes the initial detail.value, and the expression still sees the text value as input and arguments[0]. Applications that do not override it see no change. If the expression throws (for example because the CSP blocks eval()), the error is logged with Wicket.Log.error and the text value is used, so the event is still dispatched.

Migration: move the expression into a wicket-autocomplete-select listener and expose the values it needs through getChoiceData.

Backport to wicket-10.x: #1728.

Tests

  • mvn clean verify -Pjs-test -pl wicket-extensions,testing/wicket-js-tests -amd (JDK 21, after mvn clean install -DskipTests -Pfast): BUILD SUCCESS. wicket-extensions 114/114, wicket-examples 67 run with 0 failures (14 skipped), QUnit 162/162 (jQuery) and 161/161 (vanilla). japicmp and javadoc ran clean.
  • New ChoiceFilterTest: checks there is no inline onchange and that the header registers the change handler. It fails on the base code.
  • AbstractAutoCompleteRendererTest: checks getChoiceData escaping, skipping of null values, and rejection of invalid attribute names.
  • New QUnit autocomplete-test.js + autocomplete.html, wired into Gruntfile.js for both engines and listed in JAVASCRIPTTESTING.md. It covers the event and its detail, a listener replacing the value, a listener cancelling the selection, and a deprecated expression still getting the text value as arguments[0]. Against the base wicket-autocomplete.js its 3 event assertions fail.

Follow-ups

  • Next major: remove getOnSelectJavaScriptExpression and the eval() fallback (removing a protected method of AbstractAutoCompleteRenderer is binary-incompatible).
  • Separate change: core still builds string Ajax call listeners with new Function(), which also needs 'unsafe-eval'.

This change was prepared with AI assistance and reviewed and tested by me.

ChoiceFilter with autoSubmit wrote onchange="this.form.submit();" into the
markup. Wicket's default Content Security Policy (strict(): script-src
'strict-dynamic' 'nonce-...') blocks inline event handler attributes, so
under the default settings the filter silently stopped submitting on change.

It now registers the same handler with an OnEventHeaderItem, as
FilterForm#enableFocusTracking already does for focus tracking, so the
script carries the CSP nonce. The markup is unchanged apart from the removed
attribute: the select already rendered its markup id for focus tracking.

Generated-by: an AI coding assistant; reviewed and tested by the author.
The autocompleter ran AbstractAutoCompleteRenderer#getOnSelectJavaScriptExpression
by reading the onselect attribute of the chosen item and passing it to eval().
Wicket's default Content Security Policy does not allow 'unsafe-eval', so
under the default settings that eval throws and selecting an item with an
expression does nothing; the only way to use the hook was to weaken the CSP
for the whole application. The hook also invites building JavaScript from
item data by string concatenation, as its own javadoc examples do, which the
markup escaping of the attribute does not protect once eval() decodes it.

The autocompleter now dispatches a cancelable, bubbling
wicket-autocomplete-select event on the input. detail.item is the chosen
element and detail.value the text value; a listener may replace
detail.value, or call preventDefault() to leave the input unchanged (what
returning null from the expression did). Data a listener needs is written
with the new AbstractAutoCompleteRenderer#getChoiceData as escaped data-*
attributes whose names are restricted to lowercase letters, digits and
dashes. The listener is registered from renderHead() and so carries the
CSP nonce; nothing is evaluated.

getOnSelectJavaScriptExpression is deprecated for removal and keeps working
where the application allows 'unsafe-eval': its result becomes the initial
detail.value of the event. Applications that do not override it see no
difference. Migration: move the expression into a wicket-autocomplete-select
listener and expose the values it needs through getChoiceData. Removing the
method and the eval() fallback on master is a separate follow-up.

WICKET-6938

Generated-by: an AI coding assistant; reviewed and tested by the author.
@aeonframework

Copy link
Copy Markdown
Author

Hi, gentle ping on this one. It fixes WICKET-6938 (CSP-safe autocomplete). Could a maintainer take a look and approve the CI workflows when there is time? Happy to make any changes.

@aeonframework

Copy link
Copy Markdown
Author

Hi, bumping this once more in case it got lost. It fixes WICKET-6938 (CSP-safe autocomplete). The CI workflows still need a maintainer approval to run. Happy to make any changes.

@reiern70 reiern70 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aeonframework thanks for this, and sorry for the wait. I went through both commits and ran them merged onto current master: the wicket-extensions tests and the QUnit suites for both engines pass, and the new tests fail against the old code. The approach looks right to me: a cancelable, bubbling event plus data-* attributes is the natural CSP-safe replacement for the eval() hook, and the ChoiceFilter change follows what FilterForm#enableFocusTracking already does.

Points, most important first:

  1. Backport to 10.x. Both problems exist on wicket-10.x as well (the same eval() in handleSelection, the same inline onchange), and the change only adds API, so we would like it there too. Could you open a second PR against wicket-10.x with both commits? The ChoiceFilter commit cherry-picks cleanly. The autocomplete commit conflicts in three places:

    • wicket-autocomplete.js: 10.x still uses var in handleSelection.
    • Gruntfile.js: 10.x has no vanilla engine and runs the extensions pages against jQuery 4.0.0 and 3.7.1, so autocomplete.html loses its vanilla branch and gets the two jQuery URLs instead.
    • JAVASCRIPTTESTING.md does not exist on 10.x; drop that hunk.
  2. Version. 11.0.0 is already staged for its release vote, so this misses it. With the backport the API first ships in 10.12.0, so please use @since 10.12.0 on getChoiceData and since = "10.12.0" on the deprecation, on both branches.

  3. Removal follow-up. Once 11.0.0 is released it becomes the japicmp baseline for master, and removing a protected method of AbstractAutoCompleteRenderer is binary-incompatible. So "remove on master after this lands" can't happen in 11.x; it belongs in the next major. Deprecating with forRemoval = true now is still right, please just drop or reword that follow-up.

  4. Javadoc: listener and Ajax re-renders. The class example attaches the listener to document.getElementById('address'). When the field is re-rendered via Ajax the element is replaced and the listener is lost, unless the header item that registers it is contributed again. Please make clear that it should be registered from the field's own renderHead() (which runs again on re-render), or, since the event bubbles, show a listener on document that checks event.target. It would also help to mention the event on AutoCompleteTextField / AutoCompleteBehavior, which is where users look first; today it is only documented on the renderer.

  5. An empty value does not clear the field. Both callers check if (value), so a listener that sets detail.value = '' leaves the input unchanged instead of clearing it. That is inherited from the old code, and changing it would also change what deprecated expressions returning '' do, so I would just document it: an empty value leaves the input unchanged, like preventDefault().

  6. Deprecated expression under the default CSP (optional). If an application still overrides getOnSelectJavaScriptExpression and runs under the default CSP, eval() throws before the event is dispatched, so a new listener never runs and the popup stays open, as it does today. Catching the error in evalOnSelect, logging it with Wicket.Log.error and falling back to input would let an application that is halfway through the migration keep working.

  7. Nits.

    • getChoiceData Javadoc: dashed names surface in camelCase in dataset (zip-code becomes dataset.zipCode). One sentence would save users a surprise.
    • ChoiceFilter: the bind() override that calls setOutputMarkupId(true) duplicates what enableFocusTracking(choice) does right after. Harmless; keep it if you want the behavior to stand on its own.
    • WICKET-6938 is about the autocomplete only. The ChoiceFilter commit is a related CSP fix with no issue reference; a short GitHub issue for it would put it in the release notes.

With 1 to 3 addressed this looks good to me; 4 and 5 are documentation, 6 and 7 are up to you.

getChoiceData and the deprecation of getOnSelectJavaScriptExpression are
backported to wicket-10.x, so both name 10.12.0, the first release that
has them, on master too.
The class example added the listener to the input found by id. An Ajax
update that re-renders the field replaces that element and the listener
is lost with it. The example now registers the listener from the
renderHead() of the field, which runs again on every re-render, and names
the alternative of a single listener on document, which the bubbling
event reaches as well.

The documentation also says that an empty detail.value leaves the input
unchanged, like preventDefault(): the autocompleter only copies a truthy
value, as it did for the result of the deprecated expression. The
behaviour itself is unchanged.

getChoiceData says how a dashed name is read back from dataset
(zip-code becomes dataset.zipCode), and AutoCompleteTextField and
AutoCompleteBehavior point to the event, since that is where an
application looks for it.

WICKET-6938
An application that still overrides getOnSelectJavaScriptExpression and
runs under the default Content Security Policy, which does not allow
'unsafe-eval', had eval() throw before the wicket-autocomplete-select event
was dispatched. Selecting a choice then did nothing at all, as before this
change.

The autocompleter now catches the error, logs it with Wicket.Log.error and
continues with the text value of the choice, so the selection works, the
event reaches its listeners and the log shows that the expression has to be
moved into a listener. Where 'unsafe-eval' is allowed and the expression
runs, nothing changes.

WICKET-6938
@aeonframework

Copy link
Copy Markdown
Author

Thanks a lot for the careful review @reiern70! All points are addressed, as new commits on top here (no force-push), and the backport is ready.

  1. Backport to 10.x: branch csp-autocomplete-choicefilter-10.x off wicket-10.x, PR [10.x] Let autocomplete selections and ChoiceFilter auto-submit work under the default CSP (WICKET-6938) #1728. ChoiceFilter cherry-picked cleanly. For the autocomplete commit: handleSelection uses var like the rest of the 10.x file, Gruntfile.js runs autocomplete.html against jQuery 4.0.0 and 3.7.1 (no vanilla target), autocomplete.html loads jQuery plus wicket-ajax-jquery.js only, and the JAVASCRIPTTESTING.md hunk is dropped. The fix commits below are on that branch too.
  2. Version: @since 10.12.0 on getChoiceData and @Deprecated(since = "10.12.0", forRemoval = true), on both branches.
  3. Removal note: dropped. There was none in the code or Javadoc, only in the PR text and in the commit message of the autocomplete commit; I removed it from the 10.x commit and updated the PR description here. forRemoval = true stays; the removal is for the next major.
  4. Javadoc: the class example now registers the listener from the field's own renderHead() with an OnDomReadyHeaderItem, so it runs again on every re-render, and mentions the alternative of one listener on document checking event.target (the event bubbles). AutoCompleteTextField and AutoCompleteBehavior now point to the event as well.
  5. Empty value: behavior unchanged, now documented: an empty detail.value leaves the input unchanged, like preventDefault().
  6. eval() fallback: done. evalOnSelect catches the error, logs it with Wicket.Log.error and goes on with the text value, so the event is still dispatched. New QUnit test for it (an expression that throws an EvalError).
  7. Nits:
    • (a) getChoiceData Javadoc now says a dashed name is read in camelCase from dataset (zip-code becomes dataset.zipCode), and the class example uses data.zipCode.
    • (b) I kept the bind() in the ChoiceFilter behavior. enableFocusTracking is a protected method of AbstractFilter, so a subclass can override it, and the auto-submit handler should not depend on it for the markup id. It's harmless when both run.
    • (c) True, the ChoiceFilter commit has no issue of its own. Happy to file a GitHub issue for it if you'd like one to reference, just say.

Tested on both branches: the autocomplete and filter tests in wicket-extensions, plus the Grunt/QUnit run (jshint and qunit, both engines on master and both jQuery versions on 10.x), all green.

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.

2 participants