You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by pe...@apache.org on 2026/07/22 02:24:29 UTC

(wicket) branch wicket-9.x updated: WICKET-7107 move CPS headers writing to WebPage (#1503)

This is an automated email from the ASF dual-hosted git repository.

pedrosans pushed a commit to branch wicket-9.x
in repository https://gitbox.apache.org/repos/asf/wicket.git


The following commit(s) were added to refs/heads/wicket-9.x by this push:
     new 73debcf5ed WICKET-7107 move CPS headers writing to WebPage (#1503)
73debcf5ed is described below

commit 73debcf5ed7f1ae4920baf643bec98db56c58fce
Author: pedrosans <pe...@apache.org>
AuthorDate: Sat Jul 4 15:19:05 2026 -0300

    WICKET-7107 move CPS headers writing to WebPage (#1503)
---
 .../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     |  12 ++
 .../org/apache/wicket/csp/CSPHeaderWriterTest.java | 177 +++++++++++++++++++++
 .../wicket/request/IRequestHandlerDelegate.java    |  12 ++
 6 files changed, 309 insertions(+), 26 deletions(-)

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..9a8baf8bf6
--- /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;
+		}
+
+		RequestCycle 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..3585d5d839 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,8 @@ 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.csp.ContentSecurityPolicySettings;
 import org.apache.wicket.markup.MarkupType;
 import org.apache.wicket.markup.head.IHeaderResponse;
 import org.apache.wicket.markup.html.internal.HtmlHeaderContainer;
@@ -26,6 +28,7 @@ import org.apache.wicket.markup.parser.filter.HtmlHeaderSectionHandler;
 import org.apache.wicket.markup.renderStrategy.AbstractHeaderRenderStrategy;
 import org.apache.wicket.model.IModel;
 import org.apache.wicket.protocol.http.WebApplication;
+import org.apache.wicket.request.IRequestHandler;
 import org.apache.wicket.request.Request;
 import org.apache.wicket.request.Response;
 import org.apache.wicket.request.cycle.RequestCycle;
@@ -39,6 +42,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 +152,13 @@ public class WebPage extends Page
 	 */
 	protected void configureResponse(final WebResponse response)
 	{
+		ContentSecurityPolicySettings cspSettings = WebApplication.get().getCspSettings();
+		IRequestHandler 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-core/src/test/java/org/apache/wicket/csp/CSPHeaderWriterTest.java b/wicket-core/src/test/java/org/apache/wicket/csp/CSPHeaderWriterTest.java
new file mode 100644
index 0000000000..6572b41910
--- /dev/null
+++ b/wicket-core/src/test/java/org/apache/wicket/csp/CSPHeaderWriterTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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 javax.servlet.http.HttpServletResponse;
+
+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);
+
+		RequestCycle requestCycle = tester.getRequestCycle();
+
+		tester.clickLink("link_to_page_instance");
+
+		HttpServletResponse 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-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;
+	}
 }