diff --git a/pom.xml b/pom.xml index 1d16132b02d..c463c9f2145 100644 --- a/pom.xml +++ b/pom.xml @@ -820,7 +820,7 @@ ${project.groupId} ${project.artifactId} - 10.0.0 + 11.0.0 jar diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/csp/CSPHeaderWriterTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/csp/CSPHeaderWriterTest.java new file mode 100644 index 00000000000..e4911449928 --- /dev/null +++ b/wicket-core-tests/src/test/java/org/apache/wicket/csp/CSPHeaderWriterTest.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.csp; + +import org.apache.wicket.MarkupContainer; +import org.apache.wicket.RestartResponseException; +import org.apache.wicket.core.request.handler.PageProvider; +import org.apache.wicket.core.request.handler.RenderPageRequestHandler; +import org.apache.wicket.markup.IMarkupResourceStreamProvider; +import org.apache.wicket.markup.head.CssHeaderItem; +import org.apache.wicket.markup.head.IHeaderResponse; +import org.apache.wicket.markup.html.WebPage; +import org.apache.wicket.markup.html.link.StatelessLink; +import org.apache.wicket.protocol.http.BufferedWebResponse; +import org.apache.wicket.protocol.http.WebApplication; +import org.apache.wicket.protocol.http.mock.MockHttpServletResponse; +import org.apache.wicket.protocol.http.servlet.ServletWebRequest; +import org.apache.wicket.request.Response; +import org.apache.wicket.request.cycle.RequestCycle; +import org.apache.wicket.request.http.WebResponse; +import org.apache.wicket.request.mapper.parameter.PageParameters; +import org.apache.wicket.request.resource.CssResourceReference; +import org.apache.wicket.util.resource.IResourceStream; +import org.apache.wicket.util.resource.StringResourceStream; +import org.apache.wicket.util.tester.WicketTestCase; +import org.apache.wicket.util.tester.WicketTester; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.apache.wicket.csp.CSPDirective.STYLE_SRC; +import static org.apache.wicket.csp.CSPDirectiveSrcValue.SELF; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +class CSPHeaderWriterTest extends WicketTestCase +{ + + @BeforeEach + void setup() + { + tester.getApplication().getCspSettings().blocking().strict().add(STYLE_SRC, SELF); + } + + @Test + void addCspDirectiveToBufferedPage() + { + tester.startPage(Page.class); + tester.clickLink("link_to_page_instance"); + + assertThat(tester.getLastResponse().getHeader("Content-Security-Policy")).contains( + STYLE_SRC.getValue()); + } + + @Test + void dontAddCSPHeaderToRedirectResponses() + { + tester.setFollowRedirects(false); + tester.getApplication().getCspSettings().blocking().add(STYLE_SRC, SELF); + tester.startPage(Page.class); + + var requestCycle = tester.getRequestCycle(); + + tester.clickLink("link_to_page_instance"); + + var response = ((MockHttpServletResponse)requestCycle.getResponse().getContainerResponse()); + assertEquals(302, response.getStatus()); + assertFalse(response.containsHeader(CSPHeaderMode.BLOCKING.getHeader())); + } + + @Test + void addCspDirectiveToBufferedPageAfterRedirect() + { + tester.startPage(AutoRedirectPage.class); + + assertThat(tester.getLastRenderedPage()).isInstanceOf(Page.class); + assertThat(tester.getLastResponse().getHeader("Content-Security-Policy")).contains( + STYLE_SRC.getValue()); + } + + @Test + void addCspDirectiveToStatelessPageAfterRedirect() + { + tester.startPage(AlwaysRedirectPage.class); + + assertThat(tester.getLastRenderedPage()).isInstanceOf(Page.class); + assertThat(tester.getLastResponse().getHeader("Content-Security-Policy")).contains( + STYLE_SRC.getValue()); + } + + @Test + void addCspDirectiveToStatelessPageAfterNoRedirect() + { + tester.startPage(NeverRedirectPage.class); + + assertThat(tester.getLastRenderedPage()).isInstanceOf(Page.class); + assertThat(tester.getLastResponse().getHeader("Content-Security-Policy")).contains( + STYLE_SRC.getValue()); + } + + public static class Page extends WebPage implements IMarkupResourceStreamProvider + { + @Override + protected void onInitialize() + { + super.onInitialize(); + add(new StatelessLink("link_to_page_instance") + { + @Override + public void onClick() + { + setResponsePage(new Page()); + } + }); + } + + @Override + public void renderHead(IHeaderResponse response) + { + response.render(CssHeaderItem.forReference( + new CssResourceReference(CSPHeaderWriterTest.class, "style.css"), "screen")); + } + + @Override + public IResourceStream getMarkupResourceStream(MarkupContainer container, + Class containerClass) + { + return new StringResourceStream( + "link"); + } + } + + public static class AutoRedirectPage extends Page + { + public AutoRedirectPage() + { + throw new RestartResponseException(new Page()); + } + } + + public static class AlwaysRedirectPage extends Page + { + public AlwaysRedirectPage() + { + throw new RestartResponseException(Page.class, new PageParameters()); + } + } + + public static class NeverRedirectPage extends Page + { + public NeverRedirectPage() + { + throw new RestartResponseException(new PageProvider(Page.class), + RenderPageRequestHandler.RedirectPolicy.NEVER_REDIRECT); + } + } + +} diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/csp/ContentSecurityPolicySettingsTest.java similarity index 99% rename from wicket-core-tests/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java rename to wicket-core-tests/src/test/java/org/apache/wicket/csp/ContentSecurityPolicySettingsTest.java index 1fdd8cb92c4..0aa40a83ec5 100644 --- a/wicket-core-tests/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java +++ b/wicket-core-tests/src/test/java/org/apache/wicket/csp/ContentSecurityPolicySettingsTest.java @@ -49,7 +49,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -class CSPSettingRequestCycleListenerTest extends WicketTestCase +class CContentSecurityPolicySettingsTest extends WicketTestCase { @Override protected WebApplication newApplication() diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderWriter.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderWriter.java new file mode 100644 index 00000000000..f40afe50f5d --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderWriter.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.csp; + +import org.apache.wicket.request.cycle.RequestCycle; +import org.apache.wicket.request.http.WebResponse; + +/** + * Adds {@code Content-Security-Policy} and/or {@code Content-Security-Policy-Report-Only} headers + * based on the supplied configuration. + * + * @author Sven Haster + * @author Emond Papegaaij + */ +public class CSPHeaderWriter +{ + private final ContentSecurityPolicySettings settings; + + public CSPHeaderWriter(ContentSecurityPolicySettings settings) + { + this.settings = settings; + } + + public void write(WebResponse webResponse, RequestCycle cycle) + { + settings.getConfiguration().entrySet().stream().filter(entry -> entry.getValue().isSet()) + .forEach(entry -> { + CSPHeaderMode mode = entry.getKey(); + CSPHeaderConfiguration config = entry.getValue(); + String headerValue = config.renderHeaderValue(settings, cycle); + webResponse.setHeader(mode.getHeader(), headerValue); + if (config.isAddLegacyHeaders()) + { + webResponse.setHeader(mode.getLegacyHeader(), headerValue); + } + }); + } + +} \ No newline at end of file diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPRequestCycleListener.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPRequestCycleListener.java deleted file mode 100644 index a64469ded30..00000000000 --- a/wicket-core/src/main/java/org/apache/wicket/csp/CSPRequestCycleListener.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.wicket.csp; - -import org.apache.wicket.request.IRequestHandler; -import org.apache.wicket.request.IRequestHandlerDelegate; -import org.apache.wicket.request.cycle.IRequestCycleListener; -import org.apache.wicket.request.cycle.RequestCycle; -import org.apache.wicket.request.http.WebResponse; - -/** - * An {@link IRequestCycleListener} that adds {@code Content-Security-Policy} and/or - * {@code Content-Security-Policy-Report-Only} headers based on the supplied configuration. - * - * @author Sven Haster - * @author Emond Papegaaij - */ -public class CSPRequestCycleListener implements IRequestCycleListener -{ - private final ContentSecurityPolicySettings settings; - - public CSPRequestCycleListener(ContentSecurityPolicySettings settings) - { - this.settings = settings; - } - - @Override - public void onRequestHandlerResolved(RequestCycle cycle, IRequestHandler handler) - { - // WICKET-7028- this is needed for redirect to buffer use case. - protect(cycle, handler); - } - - @Override - public void onRequestHandlerExecuted(RequestCycle cycle, IRequestHandler handler) - { - protect(cycle, handler); - } - - protected void protect(RequestCycle cycle, IRequestHandler handler) - { - if (!mustProtect(handler) || !(cycle.getResponse() instanceof WebResponse)) - { - return; - } - - WebResponse webResponse = (WebResponse)cycle.getResponse(); - if (!webResponse.isHeaderSupported()) - { - return; - } - - settings.getConfiguration().entrySet().stream().filter(entry -> entry.getValue().isSet()) - .forEach(entry -> { - CSPHeaderMode mode = entry.getKey(); - CSPHeaderConfiguration config = entry.getValue(); - String headerValue = config.renderHeaderValue(settings, cycle); - webResponse.setHeader(mode.getHeader(), headerValue); - if (config.isAddLegacyHeaders()) - { - webResponse.setHeader(mode.getLegacyHeader(), headerValue); - } - }); - } - - /** - * Must the given handler be protected. - * - * @param handler - * handler - * @return true if must be protected - * @see ContentSecurityPolicySettings#mustProtectRequest(IRequestHandler) - */ - protected boolean mustProtect(IRequestHandler handler) - { - if (handler instanceof IRequestHandlerDelegate) - { - return mustProtect(((IRequestHandlerDelegate)handler).getDelegateHandler()); - } - - return settings.mustProtectRequest(handler); - } - -} diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/ContentSecurityPolicySettings.java b/wicket-core/src/main/java/org/apache/wicket/csp/ContentSecurityPolicySettings.java index 65b510b7f4f..ac34d7385c2 100644 --- a/wicket-core/src/main/java/org/apache/wicket/csp/ContentSecurityPolicySettings.java +++ b/wicket-core/src/main/java/org/apache/wicket/csp/ContentSecurityPolicySettings.java @@ -16,22 +16,20 @@ */ package org.apache.wicket.csp; -import java.util.Collections; -import java.util.EnumMap; -import java.util.Map; -import java.util.function.Predicate; -import java.util.function.Supplier; - import org.apache.wicket.Application; import org.apache.wicket.MetaDataKey; import org.apache.wicket.Page; import org.apache.wicket.core.request.handler.IPageRequestHandler; -import org.apache.wicket.core.request.handler.RenderPageRequestHandler; import org.apache.wicket.protocol.http.WebApplication; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.util.lang.Args; +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; +import java.util.function.Supplier; + /** * Build the CSP configuration like this: * @@ -69,18 +67,25 @@ public class ContentSecurityPolicySettings private final Map configs = new EnumMap<>( CSPHeaderMode.class); - private Predicate protectedFilter = RenderPageRequestHandler.class::isInstance; + private final CSPHeaderWriter cspHeaderWriter; private Supplier nonceCreator; - + public ContentSecurityPolicySettings(Application application) { Args.notNull(application, "application"); - + + cspHeaderWriter = new CSPHeaderWriter(this); + nonceCreator = () -> application.getSecuritySettings().getRandomSupplier().getRandomBase64(NONCE_LENGTH); } + public CSPHeaderWriter getHeaderWriter() + { + return cspHeaderWriter; + } + public CSPHeaderConfiguration blocking() { return configs.computeIfAbsent(CSPHeaderMode.BLOCKING, x -> new CSPHeaderConfiguration()); @@ -106,35 +111,6 @@ public ContentSecurityPolicySettings setNonceCreator(Supplier nonceCreat return this; } - /** - * Sets the predicate that determines which requests must be protected by the CSP. When the - * predicate evaluates to false, the request will not be protected. - * - * @param protectedFilter - * The new filter, must not be null. - * @return {@code this} for chaining. - */ - public ContentSecurityPolicySettings setProtectedFilter( - Predicate protectedFilter) - { - Args.notNull(protectedFilter, "protectedFilter"); - this.protectedFilter = protectedFilter; - return this; - } - - /** - * Should any request be protected by CSP. - * - * @param handler - * @return true by default for all {@link RenderPageRequestHandler}s - * - * @see #setProtectedFilter(Predicate) - */ - protected boolean mustProtectRequest(IRequestHandler handler) - { - return protectedFilter.test(handler); - } - /** * Returns true if any of the headers includes a directive with a nonce. * @@ -203,7 +179,6 @@ public Map getConfiguration() */ public void enforce(WebApplication application) { - application.getRequestCycleListeners().add(new CSPRequestCycleListener(this)); application.getHeaderResponseDecorators() .addPreResourceAggregationDecorator(response -> new CSPNonceHeaderResponseDecorator(response, this)); application.mount(new ReportCSPViolationMapper(this)); diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/WebPage.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/WebPage.java index c1e8e584f8d..a43ed80ef3c 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/WebPage.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/WebPage.java @@ -147,6 +147,12 @@ protected void renderXmlDecl() */ protected void configureResponse(final WebResponse response) { + var cspSettings = WebApplication.get().getCspSettings(); + if (cspSettings.isEnabled() && response.isHeaderSupported()) + { + cspSettings.getHeaderWriter().write(response, getRequestCycle()); + } + // Users may subclass setHeader() to set there own headers setHeaders(response);