andruhon commented on a change in pull request #376: WICKET-6682 add CSP nonce support: initial commit URL: https://github.com/apache/wicket/pull/376#discussion_r303193907
########## File path: wicket-core/src/main/java/org/apache/wicket/markup/head/filter/CspNonceHeaderResponse.java ########## @@ -0,0 +1,348 @@ +package org.apache.wicket.markup.head.filter; + +import org.apache.wicket.Application; +import org.apache.wicket.core.request.handler.IPartialPageRequestHandler; +import org.apache.wicket.core.util.string.CssUtils; +import org.apache.wicket.core.util.string.JavaScriptUtils; +import org.apache.wicket.markup.head.*; +import org.apache.wicket.markup.html.DecoratingHeaderResponse; +import org.apache.wicket.request.Response; +import org.apache.wicket.request.cycle.RequestCycle; +import org.apache.wicket.request.mapper.parameter.PageParameters; +import org.apache.wicket.request.resource.ResourceReference; +import org.apache.wicket.util.lang.Args; +import org.apache.wicket.util.string.Strings; +import org.apache.wicket.util.value.HeaderItemAttribute; +import org.apache.wicket.util.value.HeaderItemAttributeMap; + +/** + * Add CSP nonce to all relevant JavaScript and CSS header items + * <p> + * Note: please don't forget to wrap with {@link ResourceAggregator} + * when setting it up with {@link Application#setHeaderResponseDecorator}, + * otherwise dependencies will not be rendered + */ +public abstract class CspNonceHeaderResponse extends DecoratingHeaderResponse { + + public CspNonceHeaderResponse(IHeaderResponse real) { + super(real); + } + + @Override + public void render(HeaderItem item) { + while (item instanceof IWrappedHeaderItem) { + item = ((IWrappedHeaderItem)item).getWrapped(); + } + + final String nonce = getNonce(); + + if (item instanceof JavaScriptContentHeaderItem) { + item = new JavaScriptContentWithNonceHeaderItem( + ((JavaScriptContentHeaderItem) item).getJavaScript(), + ((JavaScriptContentHeaderItem) item).getId() + ).setNonce(nonce); + } if (item instanceof JavaScriptReferenceHeaderItem) { + JavaScriptReferenceHeaderItem headerItem = (JavaScriptReferenceHeaderItem) item; + item = new JavaScriptReferenceWithNonceHeaderItem( + headerItem.getReference(), + headerItem.getPageParameters(), + headerItem.getId(), + headerItem.isDefer(), + headerItem.getCharset() + ).setNonce(nonce); + } else if (item instanceof JavaScriptUrlReferenceHeaderItem) { + JavaScriptUrlReferenceHeaderItem headerItem = (JavaScriptUrlReferenceHeaderItem) item; + item = new JavaScriptUrlReferenceWithNonceHeaderItem( + headerItem.getUrl(), + headerItem.getId(), + headerItem.isDefer(), + headerItem.getCharset() + ).setNonce(nonce); + } else if (item instanceof OnDomReadyHeaderItem) { + OnDomReadyHeaderItem headerItem = (OnDomReadyHeaderItem) item; + item = new OnDomReadyHeaderWithNonceItem(headerItem.getJavaScript()).setNonce(nonce); + } else if (item instanceof OnLoadHeaderItem) { + OnLoadHeaderItem headerItem = (OnLoadHeaderItem) item; + item = new OnLoadWithNonceHeaderItem(headerItem.getJavaScript()).setNonce(nonce); + } else if (item instanceof CssContentHeaderItem) { + CssContentHeaderItem headerItem = (CssContentHeaderItem) item; + item = new CssContentHeaderWithNonceItem(headerItem.getCss(), headerItem.getId()).setNonce(nonce); + } else if (item instanceof CssReferenceHeaderItem) { + CssReferenceHeaderItem headerItem = (CssReferenceHeaderItem) item; + item = new CssReferenceWithNonceHeaderItem( + headerItem.getReference(), + headerItem.getPageParameters(), + headerItem.getMedia(), + headerItem.getRel() + ).setNonce(nonce); + } else if (item instanceof CssUrlReferenceHeaderItem) { + CssUrlReferenceHeaderItem headerItem = (CssUrlReferenceHeaderItem) item; + item = new CssUrlReferenceWithNonceHeaderItem( + headerItem.getUrl(), + headerItem.getMedia(), + headerItem.getRel() + ).setNonce(nonce); + } + + super.render(item); + } + + protected abstract String getNonce(); + + protected static void renderScriptReferenceHeaderItem( + Response response, + final CharSequence url, + final String id, + boolean defer, + String charset, + boolean async, + String nonce + ) { + Args.notEmpty(url, "url"); + Args.notEmpty(nonce, "nonce"); + boolean isAjax = RequestCycle.get().find(IPartialPageRequestHandler.class).isPresent(); + // the url needs to be escaped when Ajax, because it will break the Ajax Response XML (WICKET-4777) + CharSequence escapedUrl = isAjax ? Strings.escapeMarkup(url): url; + + HeaderItemAttributeMap attributes = new HeaderItemAttributeMap(); + attributes.add(HeaderItemAttribute.SCRIPT_SRC, String.valueOf(escapedUrl)); + attributes.add(HeaderItemAttribute.TYPE, "text/javascript"); + if (id != null) { + attributes.add(HeaderItemAttribute.ID, id); + } + if (defer) { + attributes.add(HeaderItemAttribute.SCRIPT_DEFER, "defer"); + } + if (async) { + attributes.add(HeaderItemAttribute.SCRIPT_ASYNC, "async"); + } + attributes.add(HeaderItemAttribute.CSP_NONCE, nonce); + if (!Strings.isEmpty(charset)) { + attributes.add("charset", charset); + } + + JavaScriptUtils.writeJavaScriptUrl(response, attributes); + } + + protected static void renderCssReferenceHeaderItem( + Response response, String id, String url, String media, String rel, String nonce + ) { + Args.notEmpty(url, "url"); + + HeaderItemAttributeMap attributes = new HeaderItemAttributeMap(); + if (Strings.isEmpty(rel) == false) + { + attributes.add(HeaderItemAttribute.LINK_REL, rel); + } else { + attributes.add(HeaderItemAttribute.LINK_REL, "stylesheet"); + } + + attributes.add(HeaderItemAttribute.LINK_HREF, String.valueOf(Strings.escapeMarkup(url))); + + if (Strings.isEmpty(media) == false) + { + attributes.add(HeaderItemAttribute.LINK_MEDIA, media.toString()); + } + if (Strings.isEmpty(id) == false) + { + attributes.add(HeaderItemAttribute.ID, id); + } + + attributes.add(HeaderItemAttribute.CSP_NONCE, nonce); + + CssUtils.writeLinkUrl(response, attributes); + + response.write("\n"); + } + + private static class JavaScriptContentWithNonceHeaderItem extends JavaScriptContentHeaderItem { + + private String nonce; + + public JavaScriptContentWithNonceHeaderItem(CharSequence javaScript, String id) { + super(javaScript, id, null); + } + + @Override + public void render(Response response) { + String id = getId(); + HeaderItemAttributeMap attributes = new HeaderItemAttributeMap(); + attributes.add(HeaderItemAttribute.CSP_NONCE, this.nonce); + if (id != null) { + attributes.add(HeaderItemAttribute.ID, id); + } + JavaScriptUtils.writeJavaScript(response, getJavaScript(), attributes); + } + + public JavaScriptContentWithNonceHeaderItem setNonce(String nonce) { + Args.notNull(nonce, "nonce"); + this.nonce = nonce; + return this; + } + } + + private static class JavaScriptReferenceWithNonceHeaderItem extends JavaScriptReferenceHeaderItem { + + private String nonce; + + public JavaScriptReferenceWithNonceHeaderItem(ResourceReference reference, PageParameters pageParameters, String id, boolean defer, String charset) { Review comment: Yes it can. If the the CSP policy is strict as in my example the nonce is needed for the reference like `<script src="jquery.js"></script>` and even for links such as `<link href="my.css" />`, for some reason nonce for link is not documented on developer.mozilla.org, but that's how it practically works with `script-src 'unsafe-eval' 'nonce-%s'; style-src 'nonce-%s';` ---------------------------------------------------------------- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. For queries about this service, please contact Infrastructure at: us...@infra.apache.org With regards, Apache Git Services