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;
+       }
 }

Reply via email to