This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch WW-5275-custom-csp in repository https://gitbox.apache.org/repos/asf/struts.git
commit 68a401aacfbde7ab0210cbc2503b466e08ab6264 Author: Lukasz Lenart <lukaszlen...@apache.org> AuthorDate: Sun Feb 12 16:37:10 2023 +0100 WW-5275 Allows to provide a custom CspSettings per action --- .../apache/struts2/action/CspSettingsAware.java | 33 +++++++++++++ .../struts2/interceptor/csp/CspInterceptor.java | 45 +++++++++++++----- .../struts2/interceptor/csp/CspSettings.java | 8 +++- .../interceptor/csp/DefaultCspSettings.java | 8 ++++ .../struts2/interceptor/CspInterceptorTest.java | 54 ++++++++++++++-------- 5 files changed, 116 insertions(+), 32 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/action/CspSettingsAware.java b/core/src/main/java/org/apache/struts2/action/CspSettingsAware.java new file mode 100644 index 000000000..458a7c7f3 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/action/CspSettingsAware.java @@ -0,0 +1,33 @@ +/* + * 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.struts2.action; + +import org.apache.struts2.interceptor.csp.CspSettings; + +/** + * Implement this interface by an action to provide a custom {@link CspSettings}, + * see {@link org.apache.struts2.interceptor.csp.CspInterceptor} for more details + * + * @since Struts 6.2.0 + */ +public interface CspSettingsAware { + + CspSettings getCspSettings(); + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/csp/CspInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/csp/CspInterceptor.java index 5bae4f543..8e4356646 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/csp/CspInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/csp/CspInterceptor.java @@ -20,7 +20,9 @@ package org.apache.struts2.interceptor.csp; import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.interceptor.AbstractInterceptor; -import com.opensymphony.xwork2.interceptor.PreResultListener; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.action.CspSettingsAware; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -37,20 +39,43 @@ import java.util.Optional; * @see CspSettings * @see DefaultCspSettings **/ -public final class CspInterceptor extends AbstractInterceptor implements PreResultListener { +public final class CspInterceptor extends AbstractInterceptor { - private final CspSettings settings = new DefaultCspSettings(); + private static final Logger LOG = LogManager.getLogger(CspInterceptor.class); + + private Boolean enforcingMode; + private String reportUri; @Override public String intercept(ActionInvocation invocation) throws Exception { - invocation.addPreResultListener(this); + Object action = invocation.getAction(); + if (action instanceof CspSettingsAware) { + LOG.trace("Using CspSettings provided by the action: {}", action); + applySettings(invocation, ((CspSettingsAware) action).getCspSettings()); + } else { + LOG.trace("Using DefaultCspSettings with action: {}", action); + applySettings(invocation, new DefaultCspSettings()); + } return invocation.invoke(); } - public void beforeResult(ActionInvocation invocation, String resultCode) { + private void applySettings(ActionInvocation invocation, CspSettings cspSettings) { + if (enforcingMode != null) { + LOG.trace("Applying: {} to enforcingMode", enforcingMode); + cspSettings.setEnforcingMode(enforcingMode); + } + if (reportUri != null) { + LOG.trace("Applying: {} to reportUri", reportUri); + cspSettings.setReportUri(reportUri); + } + HttpServletRequest request = invocation.getInvocationContext().getServletRequest(); HttpServletResponse response = invocation.getInvocationContext().getServletResponse(); - settings.addCspHeaders(request, response); + + invocation.addPreResultListener((actionInvocation, resultCode) -> { + LOG.trace("Applying CSP header: {} to the request", cspSettings); + cspSettings.addCspHeaders(request, response); + }); } public void setReportUri(String reportUri) { @@ -63,21 +88,19 @@ public final class CspInterceptor extends AbstractInterceptor implements PreResu throw new IllegalArgumentException("Illegal configuration: report URI is not relative to the root. Please set a report URI that starts with /"); } - settings.setReportUri(reportUri); + this.reportUri = reportUri; } private Optional<URI> buildUri(String reportUri) { try { return Optional.of(URI.create(reportUri)); } catch (IllegalArgumentException ignored) { + return Optional.empty(); } - - return Optional.empty(); } public void setEnforcingMode(String value) { - boolean enforcingMode = Boolean.parseBoolean(value); - settings.setEnforcingMode(enforcingMode); + this.enforcingMode = Boolean.parseBoolean(value); } } diff --git a/core/src/main/java/org/apache/struts2/interceptor/csp/CspSettings.java b/core/src/main/java/org/apache/struts2/interceptor/csp/CspSettings.java index adf5b5072..acb142962 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/csp/CspSettings.java +++ b/core/src/main/java/org/apache/struts2/interceptor/csp/CspSettings.java @@ -51,9 +51,13 @@ public interface CspSettings { void addCspHeaders(HttpServletRequest request, HttpServletResponse response); - // sets the uri where csp violation reports will be sent + /** + * Sets the uri where csp violation reports will be sent + */ void setReportUri(String uri); - // sets CSP headers in enforcing mode when true, and report-only when false + /** + * Sets CSP headers in enforcing mode when true, and report-only when false + */ void setEnforcingMode(boolean value); } diff --git a/core/src/main/java/org/apache/struts2/interceptor/csp/DefaultCspSettings.java b/core/src/main/java/org/apache/struts2/interceptor/csp/DefaultCspSettings.java index 199859ad4..d1768e869 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/csp/DefaultCspSettings.java +++ b/core/src/main/java/org/apache/struts2/interceptor/csp/DefaultCspSettings.java @@ -111,4 +111,12 @@ public class DefaultCspSettings implements CspSettings { this.reportUri = reportUri; } + @Override + public String toString() { + return "DefaultCspSettings{" + + "reportUri='" + reportUri + '\'' + + ", cspHeader='" + cspHeader + '\'' + + '}'; + } + } diff --git a/core/src/test/java/org/apache/struts2/interceptor/CspInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/CspInterceptorTest.java index 5727a9dcf..0dabc049c 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/CspInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/CspInterceptorTest.java @@ -22,23 +22,16 @@ import com.opensymphony.xwork2.ActionContext; import com.opensymphony.xwork2.mock.MockActionInvocation; import org.apache.logging.log4j.util.Strings; import org.apache.struts2.StrutsInternalTestCase; +import org.apache.struts2.action.CspSettingsAware; import org.apache.struts2.dispatcher.SessionMap; import org.apache.struts2.interceptor.csp.CspInterceptor; +import org.apache.struts2.interceptor.csp.CspSettings; +import org.apache.struts2.interceptor.csp.DefaultCspSettings; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import javax.servlet.http.HttpSession; -import static org.apache.struts2.interceptor.csp.CspSettings.BASE_URI; -import static org.apache.struts2.interceptor.csp.CspSettings.CSP_ENFORCE_HEADER; -import static org.apache.struts2.interceptor.csp.CspSettings.CSP_REPORT_HEADER; -import static org.apache.struts2.interceptor.csp.CspSettings.HTTP; -import static org.apache.struts2.interceptor.csp.CspSettings.HTTPS; -import static org.apache.struts2.interceptor.csp.CspSettings.NONE; -import static org.apache.struts2.interceptor.csp.CspSettings.OBJECT_SRC; -import static org.apache.struts2.interceptor.csp.CspSettings.REPORT_URI; -import static org.apache.struts2.interceptor.csp.CspSettings.SCRIPT_SRC; -import static org.apache.struts2.interceptor.csp.CspSettings.STRICT_DYNAMIC; import static org.junit.Assert.assertNotEquals; public class CspInterceptorTest extends StrutsInternalTestCase { @@ -145,28 +138,35 @@ public class CspInterceptorTest extends StrutsInternalTestCase { } } + public void testCustomPreResultListener() throws Exception { + mai.setAction(new CustomerCspAction("/report-uri")); + interceptor.setEnforcingMode("false"); + interceptor.intercept(mai); + checkHeader("/report-uri", "false"); + } + public void checkHeader(String reportUri, String enforcingMode) { String expectedCspHeader; if (Strings.isEmpty(reportUri)) { expectedCspHeader = String.format("%s '%s'; %s 'nonce-%s' '%s' %s %s; %s '%s'; ", - OBJECT_SRC, NONE, - SCRIPT_SRC, session.getAttribute("nonce"), STRICT_DYNAMIC, HTTP, HTTPS, - BASE_URI, NONE + CspSettings.OBJECT_SRC, CspSettings.NONE, + CspSettings.SCRIPT_SRC, session.getAttribute("nonce"), CspSettings.STRICT_DYNAMIC, CspSettings.HTTP, CspSettings.HTTPS, + CspSettings.BASE_URI, CspSettings.NONE ); } else { expectedCspHeader = String.format("%s '%s'; %s 'nonce-%s' '%s' %s %s; %s '%s'; %s %s", - OBJECT_SRC, NONE, - SCRIPT_SRC, session.getAttribute("nonce"), STRICT_DYNAMIC, HTTP, HTTPS, - BASE_URI, NONE, - REPORT_URI, reportUri + CspSettings.OBJECT_SRC, CspSettings.NONE, + CspSettings.SCRIPT_SRC, session.getAttribute("nonce"), CspSettings.STRICT_DYNAMIC, CspSettings.HTTP, CspSettings.HTTPS, + CspSettings.BASE_URI, CspSettings.NONE, + CspSettings.REPORT_URI, reportUri ); } String header; if (enforcingMode.equals("true")) { - header = response.getHeader(CSP_ENFORCE_HEADER); + header = response.getHeader(CspSettings.CSP_ENFORCE_HEADER); } else { - header = response.getHeader(CSP_REPORT_HEADER); + header = response.getHeader(CspSettings.CSP_REPORT_HEADER); } assertFalse("No CSP header exists", Strings.isEmpty(header)); @@ -185,4 +185,20 @@ public class CspInterceptorTest extends StrutsInternalTestCase { mai.setInvocationContext(context); session = request.getSession(); } + + private static class CustomerCspAction implements CspSettingsAware { + + private final String reportUri; + + private CustomerCspAction(String reportUri) { + this.reportUri = reportUri; + } + + @Override + public CspSettings getCspSettings() { + DefaultCspSettings settings = new DefaultCspSettings(); + settings.setReportUri(reportUri); + return settings; + } + } }