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);
 

Reply via email to