This is an automated email from the ASF dual-hosted git repository.
pedrosans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/wicket.git
The following commit(s) were added to refs/heads/master by this push:
new 5f1034c9d7 WICKET-7107 move CPS headers writing to WebPage
5f1034c9d7 is described below
commit 5f1034c9d7b66895fd0787ae14cddca366582bb9
Author: Pedro Santos <[email protected]>
AuthorDate: Wed May 6 20:19:09 2026 -0300
WICKET-7107 move CPS headers writing to WebPage
---
.../org/apache/wicket/csp/CSPHeaderWriterTest.java | 180 +++++++++++++++++++++
...java => ContentSecurityPolicySettingsTest.java} | 2 +-
.../org/apache/wicket/csp/CSPHeaderWriter.java | 61 +++++++
.../apache/wicket/csp/CSPRequestCycleListener.java | 98 -----------
.../wicket/csp/ContentSecurityPolicySettings.java | 54 ++++---
.../org/apache/wicket/markup/html/WebPage.java | 6 +
6 files changed, 279 insertions(+), 122 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..f7afcce72d
--- /dev/null
+++
b/wicket-core-tests/src/test/java/org/apache/wicket/csp/CSPHeaderWriterTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.mock.MockHttpServletResponse;
+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.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.*;
+
+class CSPHeaderWriterTest extends WicketTestCase
+{
+
+ @BeforeEach
+ void setup()
+ {
+
tester.getApplication().getCspSettings().blocking().strict().add(STYLE_SRC,
SELF);
+ }
+
+ @Test
+ void addCSPDirectiveToStatelessPage()
+ {
+ tester.startPage(Page.class);
+
+
assertTrue(tester.getLastResponse().containsHeader(CSPHeaderMode.BLOCKING.getHeader()));
+ }
+
+ @Test
+ void dontAddCSPDirectiveToStatelessPage()
+ {
+ tester.getApplication().getCspSettings().setProtectedFilter(p
-> !(p instanceof Page));
+ tester.startPage(Page.class);
+
+
assertFalse(tester.getLastResponse().containsHeader(CSPHeaderMode.BLOCKING.getHeader()));
+ }
+
+ @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.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-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 1fdd8cb92c..82972b6990 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.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
-class CSPSettingRequestCycleListenerTest extends WicketTestCase
+class ContentSecurityPolicySettingsTest 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 0000000000..32a179d497
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderWriter.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+ }
+
+ /**
+ * Writes the CSP headers for the given page
+ *
+ * @param webResponse
+ * the response to write the headers to
+ * @param cycle
+ * the current request cycle
+ */
+ 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 a64469ded3..0000000000
---
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 <code>true</code> 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 65b510b7f4..3013461acd 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,22 @@
*/
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.markup.html.WebPage;
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 +69,27 @@ public class ContentSecurityPolicySettings
private final Map<CSPHeaderMode, CSPHeaderConfiguration> configs = new
EnumMap<>(
CSPHeaderMode.class);
- private Predicate<IRequestHandler> protectedFilter =
RenderPageRequestHandler.class::isInstance;
+ private final CSPHeaderWriter cspHeaderWriter;
+
+ private Predicate<WebPage> protectedFilter = page -> true;
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());
@@ -105,17 +114,16 @@ public class ContentSecurityPolicySettings
this.nonceCreator = nonceCreator;
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.
- *
+ * Sets the predicate that determines which pages must be protected by
the CSP. When the
+ * predicate evaluates to false, the page will not be protected.
+ *
* @param protectedFilter
* The new filter, must not be null.
* @return {@code this} for chaining.
*/
- public ContentSecurityPolicySettings setProtectedFilter(
- Predicate<IRequestHandler> protectedFilter)
+ public ContentSecurityPolicySettings
setProtectedFilter(Predicate<WebPage> protectedFilter)
{
Args.notNull(protectedFilter, "protectedFilter");
this.protectedFilter = protectedFilter;
@@ -123,16 +131,17 @@ public class ContentSecurityPolicySettings
}
/**
- * Should any request be protected by CSP.
+ * Should the given page be protected by CSP.
+ *
+ * @param page
+ * the page being rendered
+ * @return <code>true</code> by default for all {@link WebPage}s
*
- * @param handler
- * @return <code>true</code> by default for all {@link
RenderPageRequestHandler}s
- *
* @see #setProtectedFilter(Predicate)
*/
- protected boolean mustProtectRequest(IRequestHandler handler)
+ public boolean mustProtect(WebPage page)
{
- return protectedFilter.test(handler);
+ return isEnabled() && protectedFilter.test(page);
}
/**
@@ -203,7 +212,6 @@ public class ContentSecurityPolicySettings
*/
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 c1e8e584f8..0f622a5070 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.mustProtect(this) &&
response.isHeaderSupported())
+ {
+ cspSettings.getHeaderWriter().write(response,
getRequestCycle());
+ }
+
// Users may subclass setHeader() to set there own headers
setHeaders(response);