Skip to content

Add opt-in Trusted Types support to CSP and the Ajax scripts - #1880

Open
Nexory wants to merge 1 commit into
apache:masterfrom
Nexory:fix/add-opt-in-trusted-types-support-to-csp-and-the
Open

Nexory wants to merge 1 commit into
apache:masterfrom
Nexory:fix/add-opt-in-trusted-types-support-to-csp-and-the

Conversation

@Nexory

@Nexory Nexory commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

CSPDirective does not know require-trusted-types-for and trusted-types, and both Ajax engines and three component scripts hand strings to DOM sinks, so every Ajax update fails on a page that enforces Trusted Types. This adds the two directives with the opt-in CSPHeaderConfiguration.requireTrustedTypes(), routes the sinks through Wicket.TrustedTypes, whose wicket policy passes server markup and scripts on unchanged, and writes the file names of MultiFileUploadField.js as text nodes. The new QUnit page trusted-types.html covers each sink under enforcement and runs with both engines.

No preset includes the directives, and without them the rendered page does not change. Under enforcement two limits remain: handlers given as strings still go through new Function and do not run, and with the jQuery engine a script element inside replaced component markup fails the update, because jQuery 4.0.0 evaluates it from a string. Tested in headless Chrome.

CSPDirective had no require-trusted-types-for and no trusted-types, so
an application could not have the browser enforce Trusted Types through
the CSP settings. Both Ajax engines, wicket-autocomplete.js,
progressbar.js and MultiFileUploadField.js also assigned plain strings
to DOM sinks (innerHTML, script text and src, DOMParser, and the XML
parsing of the Ajax response in the jQuery engine), so on a page that
enforced Trusted Types every Ajax update failed.

The two directives are now available with value checks, and
CSPHeaderConfiguration.requireTrustedTypes() adds both for the policy
name TRUSTED_TYPES_POLICY. No preset includes them. The sinks go
through Wicket.TrustedTypes, which on first use creates a policy named
wicket that passes the markup and scripts of the server on unchanged.
Without the Trusted Types API, or when the page does not allow the
policy, the values stay strings as before. MultiFileUploadField.js now
writes file names as text nodes instead of escaped markup.

An application that does not add the directives sees no change in the
rendered page. The one difference without them is in
MultiFileUploadField.js: MultiSelector.getOnlyFileNames() now returns
an array of names and getOnlyFileName() no longer escapes, which code
that calls or overrides these functions would notice.

Under enforcement two limits remain. Handlers that reach Wicket.Ajax as
strings are compiled with new Function, which the browser blocks, so
they do not run; Wicket renders its own handlers as functions. With the
jQuery engine a script element inside replaced component markup makes
the update fail, because jQuery 4.0.0 evaluates it from a string. The
user guide names both.

@papegaaij papegaaij 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.

Thank you for this contribution! Trusted Types support is something we'd really like to have in Wicket, and this is thorough work: both Ajax engines, the extensions scripts, a QUnit page that actually enforces the policy, and the user guide. I really like the direction.

Before we can merge it, two requirements have to hold.

1. With the option disabled, which is the default for now, nothing may change.
At the moment the JavaScript creates the wicket policy in every browser that supports Trusted Types, whether or not the application called requireTrustedTypes(). Applications that never opted in therefore already get TrustedHTML objects instead of strings, the replaced text xml converter and the changed MultiSelector functions. Please make the client side opt-in as well, so that the policy is only created and values are only wrapped when the application enabled the option. The inline comments have the details.

2. With the option enabled, Wicket must work, with no exceptions.
The one limitation we accept is the eval of the onselect expression in wicket-autocomplete.js. The other limitations in the guide have to go:

  • A <script> element inside replaced component markup, inline or external, must also work with the jQuery engine. That is the default engine, so this limitation would hit most applications that enable the option.
  • Please make sure nothing in Wicket's own modules hands string handlers, preconditions or dynamic extra parameters to Wicket.Ajax, so Wicket code never reaches the new Function path. The Java side renders them as functions. Application JavaScript that passes strings is the application's concern, and the guide can say so.

To show the feature works as intended, please enable it in the examples: add .requireTrustedTypes() to the CSP configuration in WicketExampleApplication and in the example applications that set up their own CSP, such as MyAuthenticatedWebApplication, and make sure the examples still work. Any script used by Wicket or by the examples that breaks under enforcement needs fixing as part of this PR.

One note on scope: custom Wicket Ajax references (setWicketAjaxReference(...)) don't need to be supported. An application that ships its own copy is responsible for keeping it in sync with the full API, so it's fine for wicket-autocomplete.js and progressbar.js to rely on Wicket.TrustedTypes.

On the tests, which are already a good base, I'd like to see:

  • a test for the disabled mode in a browser that supports Trusted Types, showing that values stay strings, for example with a custom jQuery.htmlPrefilter that does string operations;
  • an end-to-end Ajax response with <header-contribution>, <priority-evaluate> and <evaluate> under enforcement, not only a component replacement;
  • the script-in-markup test running for both engines;
  • Java tests for calling requireTrustedTypes() twice, and for combining it with strict() and reporting().

policy = { createHTML: identity, createScript: identity, createScriptURL: identity };
if (window.trustedTypes && window.trustedTypes.createPolicy) {
try {
policy = window.trustedTypes.createPolicy('wicket', policy);

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.

This runs in every browser that supports Trusted Types, including when the application did not call requireTrustedTypes(). With the option disabled, that has visible effects:

  • With jQuery 4, toMarkup passes a TrustedHTML object to jQuery, which hands it to jQuery.htmlPrefilter. Any override of that hook that does string operations throws. jquery-migrate 4.0.2, which Wicket ships, is one example. Its self-closed-tags patch is off by default, but applications that still have <div/>-style markup enable it, and it calls html.replace(...) (jquery-migrate-4.0.2.js:1017). Every Ajax update then fails with html.replace is not a function.
  • An application that enforces Trusted Types through its own header with a default policy now has Wicket's markup bypass that policy.
  • An application whose header restricts policy names without allowing wicket gets an error logged on every page, and with a report-only header a violation report on every page.

Could the policy be created, and values wrapped, only when the application enabled the option? For instance, the server could render a flag when the CSP contains trusted-types wicket. With the flag off, html(), script() and scriptURL() should return their input unchanged, and the text xml converter should not be installed. The same applies to wicket-ajax.js.

* response, so the policy 'wicket' passes them on unchanged. They stay strings where the
* browser has no Trusted Types or the page does not allow the policy.
*/
Wicket.TrustedTypes = (function () {

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.

Smaller point: html(), script() and scriptURL() turn any string into a trusted value, and any script on the page can call them. For an application that enables the option, this weakens what Trusted Types offers, because the set of code that can create trusted values is no longer small and auditable. Wicket trusts the markup and scripts the server sends, so routing the Ajax response through this policy is fine. Please at least document on this object that it must only be given markup and scripts that come from the server. The guide should also say that, with the option enabled, application code must not pass untrusted strings to Wicket.DOM.replace, Wicket.DOM.add or Wicket.Xml.parse.

const cleanedText = text.trim();

const $newElement = jQuery(cleanedText);
const $newElement = jQuery(toMarkup(cleanedText));

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.

With the option enabled, this has to work for markup that contains <script> elements, inline or external. Right now jQuery evaluates them from a string and the update fails, and this is the default engine.

One way to solve this: parse the markup the way wicket-ajax.js does, with a template element and reviveScripts, and insert the nodes with the DOM API instead of through jQuery's manipulation methods, which evaluate scripts themselves. Removed elements would still need jQuery's data and event cleanup. That would also make toMarkup and its trailing-text handling unnecessary. The same applies to add below.

});

// jQuery 4.0.0 evaluates the scripts of the markup it inserts itself, from a string
test.if('Wicket.DOM.replace - inline and external scripts in the markup', typeof(jQuery) === 'undefined', assert => {

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.

Once the jQuery engine handles scripts in markup, this test should run for both engines.

.add(CSPDirective.TRUSTED_TYPES, "myPolicy");
----

Ajax call listeners and dynamic extra parameters that reach _Wicket.Ajax_ as strings instead of functions are not executed under these directives. With the default jQuery based Ajax implementation jQuery 4 is required, and a _script_ element inside the markup of a component makes the Ajax update of that component fail, because jQuery evaluates such elements from a string. The jQuery-free implementation (_WicketAjaxResourceReference_) executes them.

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.

With the option enabled, the only limitation we want to keep is the eval of the autocomplete's onselect expression in wicket-autocomplete.js, so this paragraph should name only that one. It can also note that application JavaScript passing handlers to Wicket.Ajax as strings won't run. The script element limitation should go away with the engine change.

{
var files = inputElement.files;
var retVal = "";
var retVal = [];

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.

This changes the result of getOnlyFileNames() from a string to an array, and getOnlyFileName() below no longer escapes. Both are overridable functions of MultiSelector, so applications that never enable Trusted Types will see the change. Could you keep both functions as they are, deprecated if you like, and add a new function that returns the unescaped names as an array for addListRow to use? Building text nodes is the right approach; it's only the existing contract that should stay.

public void checkValueForDirective(CSPRenderable value,
List<CSPRenderable> existingDirectiveValues)
{
if (!(value instanceof FixedCSPValue)

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.

add(TRUSTED_TYPES, CSPDirectiveSrcValue.NONE) is rejected here, and only the string "'none'" is accepted; trustedTypesDoesNotSupportOtherValues even asserts that. Every other directive uses CSPDirectiveSrcValue.NONE for 'none', so please accept it here too.

*/
public CSPHeaderConfiguration requireTrustedTypes()
{
return add(REQUIRE_TRUSTED_TYPES_FOR, "'script'").add(TRUSTED_TYPES, TRUSTED_TYPES_POLICY);

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.

Calling this twice throws an IllegalArgumentException, because require-trusted-types-for accepts only one value. If both a library and the application call it, the application fails at startup. Could it be idempotent, skipping values that are already present?

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