Merge pull request #913 from eschulma/master

WW-5400 Extend default configuration options for the CSP interceptor.
This commit is contained in:
Lukasz Lenart
2024-05-11 06:36:33 +02:00
committed by GitHub
4 changed files with 159 additions and 18 deletions
@@ -46,6 +46,9 @@ public final class CspInterceptor extends AbstractInterceptor {
private boolean prependServletContext = true;
private boolean enforcingMode;
private String reportUri;
private String reportTo;
private String defaultCspSettingsClassName = DefaultCspSettings.class.getName();
@Override
public String intercept(ActionInvocation invocation) throws Exception {
@@ -54,8 +57,24 @@ public final class CspInterceptor extends AbstractInterceptor {
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());
LOG.trace("Using {} with action: {}", defaultCspSettingsClassName, action);
// if the defaultCspSettingsClassName is not a real class, throw an exception
try {
Class.forName(defaultCspSettingsClassName, false, Thread.currentThread().getContextClassLoader());
}
catch (ClassNotFoundException e) {
throw new IllegalArgumentException("The defaultCspSettingsClassName must be a real class.");
}
// if defaultCspSettingsClassName does not implement CspSettings, throw an exception
if (!CspSettings.class.isAssignableFrom(Class.forName(defaultCspSettingsClassName))) {
throw new IllegalArgumentException("The defaultCspSettingsClassName must implement CspSettings.");
}
CspSettings cspSettings = (CspSettings) Class.forName(defaultCspSettingsClassName)
.getDeclaredConstructor().newInstance();
applySettings(invocation, cspSettings);
}
return invocation.invoke();
}
@@ -76,6 +95,12 @@ public final class CspInterceptor extends AbstractInterceptor {
}
cspSettings.setReportUri(finalReportUri);
// apply reportTo if set
if (reportTo != null) {
LOG.trace("Applying: {} to reportTo", reportTo);
cspSettings.setReportTo(reportTo);
}
}
invocation.addPreResultListener((actionInvocation, resultCode) -> {
@@ -97,6 +122,18 @@ public final class CspInterceptor extends AbstractInterceptor {
this.reportUri = reportUri;
}
/**
* Sets the report group where csp violation reports will be sent. This will
* only be used if the reportUri is set.
*
* @param reportTo the report group where csp violation reports will be sent
*
* @since Struts 6.5.0
*/
public void setReportTo(String reportTo) {
this.reportTo = reportTo;
}
private Optional<URI> buildUri(String reportUri) {
try {
return Optional.of(URI.create(reportUri));
@@ -124,4 +161,13 @@ public final class CspInterceptor extends AbstractInterceptor {
this.prependServletContext = prependServletContext;
}
}
/**
* Sets the class name of the default {@link CspSettings} implementation to use when the action does not
* set its own values. If not set, the default is {@link DefaultCspSettings}.
*
* @since Struts 6.5.0
*/
public void setDefaultCspSettingsClassName(String defaultCspSettingsClassName) {
this.defaultCspSettingsClassName = defaultCspSettingsClassName;
}
}
@@ -37,6 +37,7 @@ public interface CspSettings {
String SCRIPT_SRC = "script-src";
String BASE_URI = "base-uri";
String REPORT_URI = "report-uri";
String REPORT_TO = "report-to";
String NONE = "none";
String STRICT_DYNAMIC = "strict-dynamic";
String HTTP = "http:";
@@ -56,6 +57,13 @@ public interface CspSettings {
*/
void setReportUri(String uri);
/**
* Sets the report group where csp violation reports will be sent
*
* @since Struts 6.5.0
*/
void setReportTo(String group);
/**
* Sets CSP headers in enforcing mode when true, and report-only when false
*/
@@ -20,6 +20,7 @@ package org.apache.struts2.interceptor.csp;
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;
@@ -31,7 +32,11 @@ import static java.lang.String.format;
/**
* Default implementation of {@link CspSettings}.
* The default policy implements strict CSP with a nonce based approach and follows the guide: <a href="https://csp.withgoogle.com/docs/index.html">https://csp.withgoogle.com/docs/index.html/</a>
* The default policy implements strict CSP with a nonce based approach and follows the guide:
* <a href="https://csp.withgoogle.com/docs/index.html">https://csp.withgoogle.com/docs/index.html/</a>
* You may extend or replace this class if you wish to customize the default policy further, and use your class
* by setting the {@link CspInterceptor} defaultCspSettingsClassName parameter. Actions that
* implement the {@link CspSettingsAware} interface will ignore the defaultCspSettingsClassName parameter.
*
* @see CspSettings
* @see CspInterceptor
@@ -42,20 +47,22 @@ public class DefaultCspSettings implements CspSettings {
private final SecureRandom sRand = new SecureRandom();
private String reportUri;
protected String reportUri;
protected String reportTo;
// default to reporting mode
private String cspHeader = CSP_REPORT_HEADER;
protected String cspHeader = CSP_REPORT_HEADER;
@Override
public void addCspHeaders(HttpServletResponse response) {
throw new UnsupportedOperationException("Unsupported implementation, use #addCspHeaders(HttpServletRequest request, HttpServletResponse response)");
}
@Override
public void addCspHeaders(HttpServletRequest request, HttpServletResponse response) {
if (isSessionActive(request)) {
LOG.trace("Session is active, applying CSP settings");
associateNonceWithSession(request);
response.setHeader(cspHeader, cratePolicyFormat(request));
response.setHeader(cspHeader, createPolicyFormat(request));
} else {
LOG.trace("Session is not active, ignoring CSP settings");
}
@@ -70,7 +77,7 @@ public class DefaultCspSettings implements CspSettings {
request.getSession().setAttribute("nonce", nonceValue);
}
private String cratePolicyFormat(HttpServletRequest request) {
protected String createPolicyFormat(HttpServletRequest request) {
StringBuilder policyFormatBuilder = new StringBuilder()
.append(OBJECT_SRC)
.append(format(" '%s'; ", NONE))
@@ -84,13 +91,18 @@ public class DefaultCspSettings implements CspSettings {
if (reportUri != null) {
policyFormatBuilder
.append(REPORT_URI)
.append(format(" %s", reportUri));
.append(format(" %s; ", reportUri));
if(reportTo != null) {
policyFormatBuilder
.append(REPORT_TO)
.append(format(" %s; ", reportTo));
}
}
return format(policyFormatBuilder.toString(), getNonceString(request));
}
private String getNonceString(HttpServletRequest request) {
protected String getNonceString(HttpServletRequest request) {
Object nonce = request.getSession().getAttribute("nonce");
return Objects.toString(nonce);
}
@@ -101,20 +113,28 @@ public class DefaultCspSettings implements CspSettings {
return ret;
}
@Override
public void setEnforcingMode(boolean enforcingMode) {
if (enforcingMode) {
cspHeader = CSP_ENFORCE_HEADER;
}
}
@Override
public void setReportUri(String reportUri) {
this.reportUri = reportUri;
}
@Override
public void setReportTo(String reportTo) {
this.reportTo = reportTo;
}
@Override
public String toString() {
return "DefaultCspSettings{" +
"reportUri='" + reportUri + '\'' +
", reportTo='" + reportTo + '\'' +
", cspHeader='" + cspHeader + '\'' +
'}';
}
@@ -31,6 +31,7 @@ import org.apache.struts2.interceptor.csp.DefaultCspSettings;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import static org.junit.Assert.assertNotEquals;
@@ -74,8 +75,10 @@ public class CspInterceptorTest extends StrutsInternalTestCase {
public void testEnforcingCspHeadersSet() throws Exception {
String reportUri = "/csp-reports";
String reportTo = "csp-group";
boolean enforcingMode = true;
interceptor.setReportUri(reportUri);
interceptor.setReportTo(reportTo);
interceptor.setEnforcingMode(enforcingMode);
session.setAttribute("nonce", "foo");
@@ -84,13 +87,15 @@ public class CspInterceptorTest extends StrutsInternalTestCase {
assertNotNull("Nonce key does not exist", session.getAttribute("nonce"));
assertFalse("Nonce value is empty", Strings.isEmpty((String) session.getAttribute("nonce")));
assertNotEquals("New nonce value couldn't be set", "foo", session.getAttribute("nonce"));
checkHeader(reportUri, enforcingMode);
checkHeader(reportUri, reportTo, enforcingMode);
}
public void testReportingCspHeadersSet() throws Exception {
String reportUri = "/csp-reports";
String reportTo = "csp-group";
boolean enforcingMode = false;
interceptor.setReportUri(reportUri);
interceptor.setReportTo(reportTo);
interceptor.setEnforcingMode(enforcingMode);
session.setAttribute("nonce", "foo");
@@ -98,7 +103,7 @@ public class CspInterceptorTest extends StrutsInternalTestCase {
assertNotNull("Nonce value is empty", session.getAttribute("nonce"));
assertNotEquals("New nonce value couldn't be set", "foo", session.getAttribute("nonce"));
checkHeader(reportUri, enforcingMode);
checkHeader(reportUri, reportTo, enforcingMode);
}
public void test_uriSetOnlyWhenSetIsCalled() throws Exception {
@@ -174,7 +179,47 @@ public class CspInterceptorTest extends StrutsInternalTestCase {
checkHeader("/report-uri", enforcingMode);
}
public void testInvalidDefaultCspSettingsClassName() throws Exception {
boolean enforcingMode = true;
mai.setAction(new TestAction());
request.setContextPath("/app");
interceptor.setEnforcingMode(enforcingMode);
interceptor.setReportUri("/report-uri");
interceptor.setPrependServletContext(false);
try {
interceptor.setDefaultCspSettingsClassName("foo");
interceptor.intercept(mai);
assert (false);
} catch (IllegalArgumentException e) {
assert (true);
}
}
public void testCustomDefaultCspSettingsClassName() throws Exception {
boolean enforcingMode = true;
mai.setAction(new TestAction());
request.setContextPath("/app");
interceptor.setEnforcingMode(enforcingMode);
interceptor.setReportUri("/report-uri");
interceptor.setPrependServletContext(false);
interceptor.setDefaultCspSettingsClassName(CustomDefaultCspSettings.class.getName());
interceptor.intercept(mai);
String header = response.getHeader(CspSettings.CSP_ENFORCE_HEADER);
// no other customization matters for this particular class
assertEquals("foo", header);
}
public void checkHeader(String reportUri, boolean enforcingMode) {
checkHeader(reportUri, null, enforcingMode);
}
public void checkHeader(String reportUri, String reportTo, boolean enforcingMode) {
String expectedCspHeader;
if (Strings.isEmpty(reportUri)) {
expectedCspHeader = String.format("%s '%s'; %s 'nonce-%s' '%s' %s %s; %s '%s'; ",
@@ -183,12 +228,23 @@ public class CspInterceptorTest extends StrutsInternalTestCase {
CspSettings.BASE_URI, CspSettings.NONE
);
} else {
expectedCspHeader = String.format("%s '%s'; %s 'nonce-%s' '%s' %s %s; %s '%s'; %s %s",
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
);
if (Strings.isEmpty(reportTo)) {
expectedCspHeader = String.format("%s '%s'; %s 'nonce-%s' '%s' %s %s; %s '%s'; %s %s; ",
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
);
}
else {
expectedCspHeader = String.format("%s '%s'; %s 'nonce-%s' '%s' %s %s; %s '%s'; %s %s; %s %s; ",
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,
CspSettings.REPORT_TO, reportTo
);
}
}
String header;
@@ -230,4 +286,15 @@ public class CspInterceptorTest extends StrutsInternalTestCase {
return settings;
}
}
/**
* Custom DefaultCspSettings class that overrides the createPolicyFormat method
* to return a fixed value.
*/
public static class CustomDefaultCspSettings extends DefaultCspSettings {
protected String createPolicyFormat(HttpServletRequest request) {
return "foo";
}
}
}