mirror of
https://github.com/apache/struts.git
synced 2026-08-06 07:06:58 +00:00
Merge pull request #913 from eschulma/master
WW-5400 Extend default configuration options for the CSP interceptor.
This commit is contained in:
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user