这是indexloc提供的服务,不要输入任何密码
Skip to content

WW-5085: Add Cross-Origin Opener Policy (COOP) and Cross-Origin Embedder Policy (COEP) support #432

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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.interceptor;

import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
import com.opensymphony.xwork2.interceptor.PreResultListener;
import com.opensymphony.xwork2.util.TextParseUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;


/**
* Interceptor that implements Cross-Origin Embedder Policy on incoming requests used to protect a
* document from loading any non-same-origin resources which don't explicitly grant the document
* permission to be loaded.
*
*
* @see <a href="https://web.dev/why-coop-coep/#coep">https://web.dev/why-coop-coep/#coep</a>
* @see <a href="https://wicg.github.io/cross-origin-embedder-policy/">https://wicg.github.io/cross-origin-embedder-policy/</a>
**/
public class CoepInterceptor extends AbstractInterceptor implements PreResultListener {

private static final Logger LOG = LoggerFactory.getLogger(CoepInterceptor.class);
private static final String REQUIRE_COEP_HEADER = "require-corp";
private static final String COEP_ENFORCING_HEADER = "Cross-Origin-Embedder-Policy";
private static final String COEP_REPORT_HEADER = "Cross-Origin-Embedder-Policy-Report-Only";

private final Set<String> exemptedPaths = new HashSet<>();
private boolean disabled = false;
private String header = COEP_ENFORCING_HEADER;

@Override
public String intercept(ActionInvocation invocation) throws Exception {
invocation.addPreResultListener(this);
return invocation.invoke();
}

@Override
public void beforeResult(ActionInvocation invocation, String resultCode) {
HttpServletRequest req = invocation.getInvocationContext().getServletRequest();
HttpServletResponse res = invocation.getInvocationContext().getServletResponse();
final String path = req.getContextPath();

if (exemptedPaths.contains(path)){
// no need to add headers
LOG.debug(String.format("Skipping COEP header for exempted path %s", path));
} else if (!disabled){
res.setHeader(header, REQUIRE_COEP_HEADER);
}
}

public void setExemptedPaths(String paths){
this.exemptedPaths.addAll(TextParseUtil.commaDelimitedStringToSet(paths));
}

public void setEnforcingMode(String mode){
boolean enforcingMode = Boolean.parseBoolean(mode);
if (enforcingMode){
header = COEP_ENFORCING_HEADER;
} else {
header = COEP_REPORT_HEADER;
}
}

public void setDisabled(String value){
disabled = Boolean.parseBoolean(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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.interceptor;

import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
import com.opensymphony.xwork2.interceptor.PreResultListener;
import com.opensymphony.xwork2.util.TextParseUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashSet;
import java.util.Set;


/**
* Interceptor that implements Cross-Origin Opener Policy on incoming requests. COOP is a mitigation against
* cross-origin information leaks and is used to make websites, cross-origin isolated. Setting the COOP header allows you to ensure that a top-level window is
* isolated from other documents by putting them in a different browsing context group, so they
* cannot directly interact with the top-level window.
*
* @see <a href="https://web.dev/why-coop-coep/#coop">https://web.dev/why-coop-coep/#coop</a>
* @see <a href="https://github.com/whatwg/html/pull/5334/files">https://github.com/whatwg/html/pull/5334/files</a>
**/
public class CoopInterceptor extends AbstractInterceptor implements PreResultListener {

private static final Logger LOG = LoggerFactory.getLogger(CoopInterceptor.class);
private static final String SAME_ORIGIN = "same-origin";
private static final String SAME_ORIGIN_ALLOW_POPUPS = "same-origin-allow-popups";
private static final String UNSAFE_NONE = "unsafe-none";
private static final String COOP_HEADER = "Cross-Origin-Opener-Policy";

private final Set<String> exemptedPaths = new HashSet<>();
private String mode = SAME_ORIGIN;

@Override
public String intercept(ActionInvocation invocation) throws Exception {
invocation.addPreResultListener(this);
return invocation.invoke();
}

@Override
public void beforeResult(ActionInvocation invocation, String resultCode) {
HttpServletRequest request = invocation.getInvocationContext().getServletRequest();
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
String path = request.getContextPath();

if (isExempted(path)){
// no need to add headers
LOG.debug(String.format("Skipping COOP header for exempted path %s", path));
} else {
response.setHeader(COOP_HEADER, getMode());
}
}

public boolean isExempted(String path){
return exemptedPaths.contains(path);
}

public void setExemptedPaths(String paths){
exemptedPaths.addAll(TextParseUtil.commaDelimitedStringToSet(paths));
}

private String getMode(){
return mode;
}

public void setMode(String mode) {
if (!(mode.equals(SAME_ORIGIN) || mode.equals(SAME_ORIGIN_ALLOW_POPUPS) || mode.equals(UNSAFE_NONE))){
throw new IllegalArgumentException(String.format("Mode '%s' not recognized!", mode));
}
this.mode = mode;
}
}
11 changes: 11 additions & 0 deletions core/src/main/resources/struts-default.xml
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,12 @@
<interceptor name="alias" class="com.opensymphony.xwork2.interceptor.AliasInterceptor"/>
<interceptor name="autowiring" class="com.opensymphony.xwork2.spring.interceptor.ActionAutowiringInterceptor"/>
<interceptor name="chain" class="com.opensymphony.xwork2.interceptor.ChainingInterceptor"/>
<interceptor name="coepInterceptor" class="org.apache.struts2.interceptor.CoepInterceptor"/>
<interceptor name="conversionError" class="org.apache.struts2.interceptor.StrutsConversionErrorInterceptor"/>
<interceptor name="cookie" class="org.apache.struts2.interceptor.CookieInterceptor"/>
<interceptor name="cookieProvider" class="org.apache.struts2.interceptor.CookieProviderInterceptor"/>
<interceptor name="clearSession" class="org.apache.struts2.interceptor.ClearSessionInterceptor" />
<interceptor name="coopInterceptor" class="org.apache.struts2.interceptor.CoopInterceptor"/>
<interceptor name="createSession" class="org.apache.struts2.interceptor.CreateSessionInterceptor" />
<interceptor name="debugging" class="org.apache.struts2.interceptor.debugging.DebuggingInterceptor" />
<interceptor name="execAndWait" class="org.apache.struts2.interceptor.ExecuteAndWaitInterceptor"/>
Expand Down Expand Up @@ -389,6 +391,15 @@
<interceptor-ref name="actionMappingParams"/>
<interceptor-ref name="params"/>
<interceptor-ref name="conversionError"/>
<interceptor-ref name="coepInterceptor">
<param name="enforcingMode">false</param>
<param name="disabled">false</param>
<param name="exemptedPaths"></param>
</interceptor-ref>
<interceptor-ref name="coopInterceptor">
<param name="exemptedPaths"></param>
<param name="mode">same-origin</param>
</interceptor-ref>
<interceptor-ref name="fetchMetadata"/>
<interceptor-ref name="validation">
<param name="excludeMethods">input,back,cancel,browse</param>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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.interceptor;

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.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

import java.util.HashMap;
import java.util.Map;

public class CoepInterceptorTest extends StrutsInternalTestCase {

private final CoepInterceptor interceptor = new CoepInterceptor();
private final MockActionInvocation mai = new MockActionInvocation();
private final MockHttpServletRequest request = new MockHttpServletRequest();
private final MockHttpServletResponse response = new MockHttpServletResponse();

private final String COEP_ENFORCING_HEADER = "Cross-Origin-Embedder-Policy";
private final String COEP_REPORT_HEADER = "Cross-Origin-Embedder-Policy-Report-Only";
private final String HEADER_CONTENT = "require-corp";


public void testDisabled() throws Exception {
interceptor.setDisabled("true");

interceptor.intercept(mai);

String header = response.getHeader(COEP_ENFORCING_HEADER);
assertTrue("COEP is not disabled", Strings.isEmpty(header));
}

public void testEnforcingHeader() throws Exception {
interceptor.setEnforcingMode("true");

interceptor.intercept(mai);

String header = response.getHeader(COEP_ENFORCING_HEADER);
assertFalse("COEP enforcing header does not exist", Strings.isEmpty(header));
assertEquals("COEP header value is incorrect", HEADER_CONTENT, header);
}

public void testExemptedPath() throws Exception{
request.setContextPath("/foo");
interceptor.setEnforcingMode("true");

interceptor.intercept(mai);

String header = response.getHeader(COEP_ENFORCING_HEADER);
assertTrue("COEP applied to exempted path", Strings.isEmpty(header));
}

public void testReportingHeader() throws Exception {
interceptor.setEnforcingMode("false");

interceptor.intercept(mai);

String header = response.getHeader(COEP_REPORT_HEADER);
assertFalse("COEP reporting header does not exist", Strings.isEmpty(header));
assertEquals("COEP header value is incorrect", HEADER_CONTENT, header);
}

@Override
protected void setUp() throws Exception {
super.setUp();
container.inject(interceptor);
interceptor.setExemptedPaths("/foo");
ServletActionContext.setRequest(request);
ServletActionContext.setResponse(response);
ActionContext context = ServletActionContext.getActionContext();
Map<String, Object> session = new HashMap<>();
context.withSession(session);
mai.setInvocationContext(context);
}

}
Loading