Skip to content

Commit

Permalink
WW-5275 Allows to provide a custom CspSettings per action
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszlenart committed Feb 12, 2023
1 parent 930c6de commit 68a401a
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 32 deletions.
33 changes: 33 additions & 0 deletions core/src/main/java/org/apache/struts2/action/CspSettingsAware.java
Original file line number Diff line number Diff line change
@@ -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();

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

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;
Expand All @@ -37,20 +39,43 @@
* @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) {
Expand All @@ -63,21 +88,19 @@ public void setReportUri(String reportUri) {
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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,12 @@ public void setReportUri(String reportUri) {
this.reportUri = reportUri;
}

@Override
public String toString() {
return "DefaultCspSettings{" +
"reportUri='" + reportUri + '\'' +
", cspHeader='" + cspHeader + '\'' +
'}';
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,16 @@
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 {
Expand Down Expand Up @@ -145,28 +138,35 @@ public void testCannotParseRelativeUri() {
}
}

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));
Expand All @@ -185,4 +185,20 @@ protected void setUp() throws Exception {
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;
}
}
}

0 comments on commit 68a401a

Please sign in to comment.