This is an automated email from the ASF dual-hosted git repository.
bitstorm pushed a commit to branch wicket-10.x
in repository https://gitbox.apache.org/repos/asf/wicket.git
The following commit(s) were added to refs/heads/wicket-10.x by this push:
new 77315b6ba7 WICKET-7107 move CPS headers writing to WebPage (#1503)
77315b6ba7 is described below
commit 77315b6ba72726a10bc92394de67ed2749e30649
Author: pedrosans <[email protected]>
AuthorDate: Sat Jul 4 15:19:05 2026 -0300
WICKET-7107 move CPS headers writing to WebPage (#1503)
---
.../org/apache/wicket/csp/CSPHeaderWriterTest.java | 175 +++++++++++++++++++++
.../org/apache/wicket/csp/CSPHeaderWriter.java | 61 +++++++
.../apache/wicket/csp/CSPRequestCycleListener.java | 34 ++--
.../wicket/csp/ContentSecurityPolicySettings.java | 39 +++--
.../org/apache/wicket/markup/html/WebPage.java | 10 ++
.../wicket/request/IRequestHandlerDelegate.java | 12 ++
6 files changed, 305 insertions(+), 26 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..2da62b6f5a
--- /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.IRequestHandler;
+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, IRequestHandler handler)
+ {
+ if (!settings.mustProtectRequest(handler) ||
!webResponse.isHeaderSupported())
+ {
+ return;
+ }
+
+ var cycle = RequestCycle.get();
+
+ 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
index a64469ded3..7c7ec9ac13 100644
---
a/wicket-core/src/main/java/org/apache/wicket/csp/CSPRequestCycleListener.java
+++
b/wicket-core/src/main/java/org/apache/wicket/csp/CSPRequestCycleListener.java
@@ -16,18 +16,23 @@
*/
package org.apache.wicket.csp;
+import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
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;
+import static org.apache.wicket.request.IRequestHandlerDelegate.unwrap;
+
/**
* 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
+ * @see CSPHeaderWriter
+ * @deprecated
*/
public class CSPRequestCycleListener implements IRequestCycleListener
{
@@ -53,33 +58,30 @@ public class CSPRequestCycleListener implements
IRequestCycleListener
protected void protect(RequestCycle cycle, IRequestHandler handler)
{
- if (!mustProtect(handler) || !(cycle.getResponse() instanceof
WebResponse))
+ /*
+ * page request handlers are protected during page rendering,
+ * not at this point.
+ * it's important to avoid hooking the protection here
+ * because to call response.reset() during rendering is a
+ * valid use case that would end up undoing the protection
+ * made inside this request listener
+ */
+ if (unwrap(handler) instanceof RenderPageRequestHandler)
{
return;
}
- WebResponse webResponse = (WebResponse)cycle.getResponse();
- if (!webResponse.isHeaderSupported())
+ if (!(cycle.getResponse() instanceof WebResponse))
{
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);
- }
- });
+
settings.getHeaderWriter().write((WebResponse)cycle.getResponse(), handler);
}
/**
* Must the given handler be protected.
- *
+ *
* @param handler
* handler
* @return <code>true</code> if must be protected
@@ -91,7 +93,7 @@ public class CSPRequestCycleListener implements
IRequestCycleListener
{
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..2cebf9ed7a 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,12 +16,6 @@
*/
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;
@@ -30,8 +24,17 @@ 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.request.http.WebResponse;
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;
+
+import static org.apache.wicket.request.IRequestHandlerDelegate.unwrap;
+
/**
* Build the CSP configuration like this:
*
@@ -71,16 +74,26 @@ public class ContentSecurityPolicySettings
private Predicate<IRequestHandler> protectedFilter =
RenderPageRequestHandler.class::isInstance;
+ 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());
@@ -113,6 +126,9 @@ public class ContentSecurityPolicySettings
* @param protectedFilter
* The new filter, must not be null.
* @return {@code this} for chaining.
+ *
+ * @deprecated
+ * @see org.apache.wicket.markup.html.WebPage#configureResponse
*/
public ContentSecurityPolicySettings setProtectedFilter(
Predicate<IRequestHandler> protectedFilter)
@@ -127,12 +143,15 @@ public class ContentSecurityPolicySettings
*
* @param handler
* @return <code>true</code> by default for all {@link
RenderPageRequestHandler}s
- *
+ *
* @see #setProtectedFilter(Predicate)
+ *
+ * @deprecated
+ * @see org.apache.wicket.markup.html.WebPage#configureResponse
*/
protected boolean mustProtectRequest(IRequestHandler handler)
{
- return protectedFilter.test(handler);
+ return protectedFilter.test(unwrap(handler));
}
/**
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..7b47ba05fa 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
@@ -18,6 +18,7 @@ package org.apache.wicket.markup.html;
import org.apache.wicket.Component;
import org.apache.wicket.Page;
+import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
import org.apache.wicket.markup.MarkupType;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.html.internal.HtmlHeaderContainer;
@@ -39,6 +40,8 @@ import org.apache.wicket.util.visit.IVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import static org.apache.wicket.request.IRequestHandlerDelegate.unwrap;
+
/**
* Base class for HTML pages. This subclass of Page simply returns HTML when
asked for its markup
@@ -147,6 +150,13 @@ public class WebPage extends Page
*/
protected void configureResponse(final WebResponse response)
{
+ var cspSettings = WebApplication.get().getCspSettings();
+ var handler = RequestCycle.get().getActiveRequestHandler();
+ if (cspSettings.isEnabled() && unwrap(handler) instanceof
RenderPageRequestHandler)
+ {
+ cspSettings.getHeaderWriter().write(response, handler);
+ }
+
// Users may subclass setHeader() to set there own headers
setHeaders(response);
diff --git
a/wicket-request/src/main/java/org/apache/wicket/request/IRequestHandlerDelegate.java
b/wicket-request/src/main/java/org/apache/wicket/request/IRequestHandlerDelegate.java
index a00461606c..de3803dd99 100644
---
a/wicket-request/src/main/java/org/apache/wicket/request/IRequestHandlerDelegate.java
+++
b/wicket-request/src/main/java/org/apache/wicket/request/IRequestHandlerDelegate.java
@@ -25,4 +25,16 @@ public interface IRequestHandlerDelegate extends
IRequestHandler
* @return the delegate {@link IRequestHandler}
*/
IRequestHandler getDelegateHandler();
+
+ /**
+ * @return the innermost delegated {@link IRequestHandler}
+ */
+ static IRequestHandler unwrap(IRequestHandler handler)
+ {
+ if (handler instanceof IRequestHandlerDelegate)
+ {
+ return
((IRequestHandlerDelegate)handler).getDelegateHandler();
+ }
+ return handler;
+ }
}