Skip to content

Commit

Permalink
Merge 2707bf0 into 2a03047
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszlenart committed Aug 23, 2022
2 parents 2a03047 + 2707bf0 commit cd49709
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 76 deletions.
Expand Up @@ -23,6 +23,7 @@
import com.opensymphony.xwork2.interceptor.PreResultListener;
import java.net.URI;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
Expand All @@ -36,6 +37,7 @@
* @see DefaultCspSettings
**/
public final class CspInterceptor extends AbstractInterceptor implements PreResultListener {

private final CspSettings settings = new DefaultCspSettings();

@Override
Expand All @@ -45,8 +47,9 @@ public String intercept(ActionInvocation invocation) throws Exception {
}

public void beforeResult(ActionInvocation invocation, String resultCode) {
HttpServletRequest request = invocation.getInvocationContext().getServletRequest();
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
settings.addCspHeaders(response);
settings.addCspHeaders(request, response);
}

public void setReportUri(String reportUri) {
Expand Down
Expand Up @@ -18,6 +18,7 @@
*/
package org.apache.struts2.interceptor.csp;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
Expand All @@ -42,9 +43,17 @@ public interface CspSettings {
String HTTPS = "https:";
String CSP_REPORT_TYPE = "application/csp-report";

/**
* @deprecated use {@link #addCspHeaders(HttpServletRequest, HttpServletResponse)} instead
*/
@Deprecated
void addCspHeaders(HttpServletResponse response);

void addCspHeaders(HttpServletRequest request, HttpServletResponse response);

// 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
void setEnforcingMode(boolean value);
}
Expand Up @@ -18,13 +18,14 @@
*/
package org.apache.struts2.interceptor.csp;

import com.opensymphony.xwork2.ActionContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Map;
import java.util.function.Supplier;
import java.util.Objects;

import static java.lang.String.format;

Expand All @@ -37,50 +38,61 @@
*/
public class DefaultCspSettings implements CspSettings {

private final SecureRandom sRand = new SecureRandom();
private final static Logger LOG = LogManager.getLogger(DefaultCspSettings.class);

// this supplier computes a policy format
private final Supplier<String> lazyPolicyBuilder = new Supplier<String>() {
@Override
public String get() {
StringBuilder policyFormatBuilder = new StringBuilder()
.append(OBJECT_SRC)
.append(format(" '%s'; ", NONE))
.append(SCRIPT_SRC)
.append(" 'nonce-%s' ") // nonce placeholder
.append(format("'%s' ", STRICT_DYNAMIC))
.append(format("%s %s; ", HTTP, HTTPS))
.append(BASE_URI)
.append(format(" '%s'; ", NONE));

if (reportUri != null) {
policyFormatBuilder
.append(REPORT_URI)
.append(format(" %s", reportUri));
}

return format(policyFormatBuilder.toString(), getNonceString());
}
};
private final SecureRandom sRand = new SecureRandom();

private String reportUri;
// default to reporting mode
private String cspHeader = CSP_REPORT_HEADER;

@Override
public void addCspHeaders(HttpServletResponse response) {
associateNonceWithSession();
response.setHeader(cspHeader, lazyPolicyBuilder.get());
throw new UnsupportedOperationException("Unsupported implementation, use #addCspHeaders(HttpServletRequest request, HttpServletResponse response)");
}

private String getNonceString() {
Map<String, Object> session = ActionContext.getContext().getSession();
return (String) session.get("nonce");
public void addCspHeaders(HttpServletRequest request, HttpServletResponse response) {
if (isSessionActive(request)) {
LOG.debug("Session is active, applying CSP settings");
associateNonceWithSession(request);
response.setHeader(cspHeader, cratePolicyFormat(request));
} else {
LOG.debug("Session is not active, ignoring CSP settings");
}
}

private void associateNonceWithSession() {
Map<String, Object> session = ActionContext.getContext().getSession();
private boolean isSessionActive(HttpServletRequest request) {
return request.getSession(false) != null;
}

private void associateNonceWithSession(HttpServletRequest request) {
String nonceValue = Base64.getUrlEncoder().encodeToString(getRandomBytes());
session.put("nonce", nonceValue);
request.getSession().setAttribute("nonce", nonceValue);
}

private String cratePolicyFormat(HttpServletRequest request) {
StringBuilder policyFormatBuilder = new StringBuilder()
.append(OBJECT_SRC)
.append(format(" '%s'; ", NONE))
.append(SCRIPT_SRC)
.append(" 'nonce-%s' ") // nonce placeholder
.append(format("'%s' ", STRICT_DYNAMIC))
.append(format("%s %s; ", HTTP, HTTPS))
.append(BASE_URI)
.append(format(" '%s'; ", NONE));

if (reportUri != null) {
policyFormatBuilder
.append(REPORT_URI)
.append(format(" %s", reportUri));
}

return format(policyFormatBuilder.toString(), getNonceString(request));
}

private String getNonceString(HttpServletRequest request) {
Object nonce = request.getSession().getAttribute("nonce");
return Objects.toString(nonce);
}

private byte[] getRandomBytes() {
Expand All @@ -98,4 +110,5 @@ public void setEnforcingMode(boolean enforcingMode) {
public void setReportUri(String reportUri) {
this.reportUri = reportUri;
}

}
Expand Up @@ -21,24 +21,34 @@
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.mock.MockActionInvocation;
import org.apache.logging.log4j.util.Strings;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.dispatcher.SessionMap;
import org.apache.struts2.interceptor.csp.CspInterceptor;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpSession;

import static org.apache.struts2.interceptor.csp.CspSettings.*;
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 {

private final CspInterceptor interceptor = new CspInterceptor();
private final MockActionInvocation mai = new MockActionInvocation();
private final MockHttpServletRequest request = new MockHttpServletRequest();
private final MockHttpServletResponse response = new MockHttpServletResponse();
private final Map<String, Object> session = new HashMap<>();

private HttpSession session;

public void test_whenRequestReceived_thenNonceIsSetInSession_andCspHeaderContainsIt() throws Exception {
String reportUri = "/barfoo";
Expand All @@ -48,8 +58,8 @@ public void test_whenRequestReceived_thenNonceIsSetInSession_andCspHeaderContain

interceptor.intercept(mai);

assertTrue("Nonce key does not exist", session.containsKey("nonce"));
assertFalse("Nonce value is empty", Strings.isEmpty((String) session.get("nonce")));
assertNotNull("Nonce key does not exist", session.getAttribute("nonce"));
assertFalse("Nonce value is empty", Strings.isEmpty((String) session.getAttribute("nonce")));
checkHeader(reportUri, reporting);
}

Expand All @@ -58,13 +68,13 @@ public void test_whenNonceAlreadySetInSession_andRequestReceived_thenNewNonceIsS
String enforcingMode = "true";
interceptor.setReportUri(reportUri);
interceptor.setEnforcingMode(enforcingMode);
session.put("nonce", "foo");
session.setAttribute("nonce", "foo");

interceptor.intercept(mai);

assertTrue("Nonce key does not exist", session.containsKey("nonce"));
assertFalse("Nonce value is empty", Strings.isEmpty((String) session.get("nonce")));
assertFalse("New nonce value couldn't be set", session.get("nonce").equals("foo"));
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);
}

Expand All @@ -73,13 +83,13 @@ public void testEnforcingCspHeadersSet() throws Exception {
String enforcingMode = "true";
interceptor.setReportUri(reportUri);
interceptor.setEnforcingMode(enforcingMode);
session.put("nonce", "foo");
session.setAttribute("nonce", "foo");

interceptor.intercept(mai);

assertTrue("Nonce key does not exist", session.containsKey("nonce"));
assertFalse("Nonce value is empty", Strings.isEmpty((String) session.get("nonce")));
assertFalse("New nonce value couldn't be set", session.get("nonce").equals("foo"));
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);
}

Expand All @@ -88,13 +98,12 @@ public void testReportingCspHeadersSet() throws Exception {
String enforcingMode = "false";
interceptor.setReportUri(reportUri);
interceptor.setEnforcingMode(enforcingMode);
session.put("nonce", "foo");
session.setAttribute("nonce", "foo");

interceptor.intercept(mai);

assertTrue("Nonce key does not exist", session.containsKey("nonce"));
assertFalse("Nonce value is empty", Strings.isEmpty((String) session.get("nonce")));
assertFalse("New nonce value couldn't be set", session.get("nonce").equals("foo"));
assertNotNull("Nonce value is empty", session.getAttribute("nonce"));
assertNotEquals("New nonce value couldn't be set", "foo", session.getAttribute("nonce"));
checkHeader(reportUri, enforcingMode);
}

Expand All @@ -116,45 +125,45 @@ public void testCannotParseUri() throws Exception {
String enforcingMode = "false";
interceptor.setEnforcingMode(enforcingMode);

try{
try {
interceptor.setReportUri("ww w. google.@com");
assert(false);
} catch (IllegalArgumentException e){
assert(true);
assert (false);
} catch (IllegalArgumentException e) {
assert (true);
}
}

public void testCannotParseRelativeUri() throws Exception {
String enforcingMode = "false";
interceptor.setEnforcingMode(enforcingMode);

try{
try {
interceptor.setReportUri("some-uri");
assert(false);
} catch (IllegalArgumentException e){
assert(true);
assert (false);
} catch (IllegalArgumentException e) {
assert (true);
}
}

public void checkHeader(String reportUri, String enforcingMode){
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.get("nonce"), STRICT_DYNAMIC, HTTP, HTTPS,
BASE_URI, NONE
OBJECT_SRC, NONE,
SCRIPT_SRC, session.getAttribute("nonce"), STRICT_DYNAMIC, HTTP, HTTPS,
BASE_URI, NONE
);
} else {
expectedCspHeader = String.format("%s '%s'; %s 'nonce-%s' '%s' %s %s; %s '%s'; %s %s",
OBJECT_SRC, NONE,
SCRIPT_SRC, session.get("nonce"), STRICT_DYNAMIC, HTTP, HTTPS,
BASE_URI, NONE,
REPORT_URI, reportUri
OBJECT_SRC, NONE,
SCRIPT_SRC, session.getAttribute("nonce"), STRICT_DYNAMIC, HTTP, HTTPS,
BASE_URI, NONE,
REPORT_URI, reportUri
);
}

String header = "";
if (enforcingMode.equals("true")){
if (enforcingMode.equals("true")) {
header = response.getHeader(CSP_ENFORCE_HEADER);
} else {
header = response.getHeader(CSP_REPORT_HEADER);
Expand All @@ -168,10 +177,12 @@ public void checkHeader(String reportUri, String enforcingMode){
protected void setUp() throws Exception {
super.setUp();
container.inject(interceptor);
ServletActionContext.setRequest(request);
ServletActionContext.setResponse(response);
ActionContext context = ServletActionContext.getActionContext().bind();
context.withSession(session);
ActionContext context = ActionContext.getContext()
.withServletRequest(request)
.withServletResponse(response)
.withSession(new SessionMap<>(request))
.bind();
mai.setInvocationContext(context);
session = request.getSession();
}
}
Expand Up @@ -259,6 +259,8 @@ public void testNonce() throws Exception {
EasyMock.replay(servletContext);

init();
// create session
request.getSession();

request.setRequestURI("/tutorial/test10.action");
ActionMapping mapping = container.getInstance(ActionMapper.class).getMapping(request, configurationManager);
Expand Down

0 comments on commit cd49709

Please sign in to comment.