This is an automated email from the ASF dual-hosted git repository. pedrosans pushed a commit to branch WICKET-7107-backport in repository https://gitbox.apache.org/repos/asf/wicket.git
commit d970362e436fc5aecb51f7c1b9be654282b001d0 Author: Pedro Santos <[email protected]> AuthorDate: Wed May 6 20:52:23 2026 -0300 WICKET-7107 move CPS headers writing to WebPage --- .../org/apache/wicket/csp/CSPHeaderWriterTest.java | 175 +++++++++++++++++++++ .../org/apache/wicket/csp/CSPHeaderWriter.java | 53 +++++++ .../wicket/csp/ContentSecurityPolicySettings.java | 29 ++-- .../org/apache/wicket/markup/html/WebPage.java | 6 + 4 files changed, 253 insertions(+), 10 deletions(-) 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 0000000000..e491144992 --- /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<Void>("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( + "<html><head></head><body><a wicket:id=\"link_to_page_instance\">link</a></body></html>"); + } + } + + 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/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 0000000000..f40afe50f5 --- /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/ContentSecurityPolicySettings.java b/wicket-core/src/main/java/org/apache/wicket/csp/ContentSecurityPolicySettings.java index 65b510b7f4..1e96d52b1f 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,21 @@ */ 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.Predicate; +import java.util.function.Supplier; + /** * Build the CSP configuration like this: * @@ -69,18 +68,28 @@ public class ContentSecurityPolicySettings private final Map<CSPHeaderMode, CSPHeaderConfiguration> configs = new EnumMap<>( CSPHeaderMode.class); - private Predicate<IRequestHandler> protectedFilter = RenderPageRequestHandler.class::isInstance; + private Predicate<IRequestHandler> protectedFilter = (handler) -> false; + + private final CSPHeaderWriter cspHeaderWriter; + private Supplier<String> 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()); 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 c1e8e584f8..a43ed80ef3 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 @@ public class WebPage extends Page */ 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);
