Skip to content

Commit 4acaccf

Browse files
authored
Merge branch 'master' into coop-coep-post
2 parents 529ec1e + a55e9ed commit 4acaccf

File tree

16 files changed

+973
-0
lines changed

16 files changed

+973
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.struts2.interceptor;
20+
21+
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_DEST_HEADER;
22+
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_MODE_HEADER;
23+
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_SITE_HEADER;
24+
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.VARY_HEADER;
25+
26+
import com.opensymphony.xwork2.ActionContext;
27+
import com.opensymphony.xwork2.ActionInvocation;
28+
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
29+
import com.opensymphony.xwork2.interceptor.PreResultListener;
30+
import com.opensymphony.xwork2.util.TextParseUtil;
31+
import java.util.HashSet;
32+
import java.util.Set;
33+
import javax.servlet.http.HttpServletRequest;
34+
import javax.servlet.http.HttpServletResponse;
35+
import org.apache.logging.log4j.LogManager;
36+
import org.apache.logging.log4j.Logger;
37+
38+
/**
39+
* Interceptor that implements Fetch Metadata policy on incoming requests used to protect against
40+
* CSRF, XSSI, and cross-origin information leaks. Uses {@link StrutsResourceIsolationPolicy} to
41+
* filter the requests allowed to be processed.
42+
*
43+
* @see <a href="https://web.dev/fetch-metadata/">https://web.dev/fetch-metadata/</a>
44+
**/
45+
46+
public class FetchMetadataInterceptor extends AbstractInterceptor {
47+
private static final Logger logger = LogManager.getLogger(FetchMetadataInterceptor.class);
48+
private static final String VARY_HEADER_VALUE = String.format("%s,%s,%s", SEC_FETCH_DEST_HEADER, SEC_FETCH_SITE_HEADER, SEC_FETCH_MODE_HEADER);
49+
private static final String SC_FORBIDDEN = String.valueOf(HttpServletResponse.SC_FORBIDDEN);
50+
51+
private final Set<String> exemptedPaths = new HashSet<>();
52+
private final ResourceIsolationPolicy resourceIsolationPolicy = new StrutsResourceIsolationPolicy();
53+
54+
public void setExemptedPaths(String paths){
55+
this.exemptedPaths.addAll(TextParseUtil.commaDelimitedStringToSet(paths));
56+
}
57+
58+
@Override
59+
public String intercept(ActionInvocation invocation) throws Exception {
60+
ActionContext context = invocation.getInvocationContext();
61+
HttpServletRequest request = context.getServletRequest();
62+
63+
addVaryHeaders(invocation);
64+
65+
String contextPath = request.getContextPath();
66+
// Apply exemptions: paths/endpoints meant to be served cross-origin
67+
if (exemptedPaths.contains(contextPath)) {
68+
return invocation.invoke();
69+
}
70+
71+
// Check if request is allowed
72+
if (resourceIsolationPolicy.isRequestAllowed(request)) {
73+
return invocation.invoke();
74+
}
75+
76+
logger.atDebug().log(
77+
"Fetch metadata rejected cross-origin request to %s",
78+
contextPath
79+
);
80+
return SC_FORBIDDEN;
81+
}
82+
83+
private void addVaryHeaders(ActionInvocation invocation) {
84+
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
85+
response.setHeader(VARY_HEADER, VARY_HEADER_VALUE);
86+
}
87+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.struts2.interceptor;
20+
21+
import javax.servlet.http.HttpServletRequest;
22+
23+
/**
24+
* Interface for the resource isolation policies to be used for fetch metadata checks.
25+
*
26+
* Resource isolation policies are designed to protect against cross origin attacks and use the
27+
* {@code sec-fetch-*} request headers to decide whether to accept or reject a request. Read more
28+
* about <a href="https://web.dev/fetch-metadata/">Fetch Metadata.</a>
29+
*
30+
* See {@link StrutsResourceIsolationPolicy} for the default implementation used.
31+
*
32+
* @see <a href="https://web.dev/fetch-metadata/">https://web.dev/fetch-metadata/</a>
33+
**/
34+
35+
@FunctionalInterface
36+
public interface ResourceIsolationPolicy {
37+
String SEC_FETCH_SITE_HEADER = "sec-fetch-site";
38+
String SEC_FETCH_MODE_HEADER = "sec-fetch-mode";
39+
String SEC_FETCH_DEST_HEADER = "sec-fetch-dest";
40+
String VARY_HEADER = "Vary";
41+
String SAME_ORIGIN = "same-origin";
42+
String SAME_SITE = "same-site";
43+
String NONE = "none";
44+
String MODE_NAVIGATE = "navigate";
45+
String DEST_OBJECT = "object";
46+
String DEST_EMBED = "embed";
47+
String CROSS_SITE = "cross-site";
48+
String CORS = "cors";
49+
String DEST_SCRIPT = "script";
50+
String DEST_IMAGE = "image";
51+
52+
boolean isRequestAllowed(HttpServletRequest request);
53+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.struts2.interceptor;
20+
21+
import org.apache.logging.log4j.util.Strings;
22+
23+
import javax.servlet.http.HttpServletRequest;
24+
25+
/**
26+
*
27+
* Default resource isolation policy used in {@link FetchMetadataInterceptor} that
28+
* implements the {@link ResourceIsolationPolicy} interface. This default policy is based on
29+
* <a href="https://web.dev/fetch-metadata/">https://web.dev/fetch-metadata/</a>.
30+
*
31+
* @see <a href="https://web.dev/fetch-metadata/">https://web.dev/fetch-metadata/</a>
32+
**/
33+
34+
public final class StrutsResourceIsolationPolicy implements ResourceIsolationPolicy {
35+
36+
@Override
37+
public boolean isRequestAllowed(HttpServletRequest request) {
38+
String site = request.getHeader(SEC_FETCH_SITE_HEADER);
39+
40+
// Allow requests from browsers which don't send Fetch Metadata
41+
if (Strings.isEmpty(site)){
42+
return true;
43+
}
44+
45+
// Allow same-site and browser-initiated requests
46+
if (SAME_ORIGIN.equals(site) || SAME_SITE.equals(site) || NONE.equals(site)) {
47+
return true;
48+
}
49+
50+
// Allow simple top-level navigations except <object> and <embed>
51+
return isAllowedTopLevelNavigation(request);
52+
}
53+
54+
private boolean isAllowedTopLevelNavigation(HttpServletRequest request) {
55+
String mode = request.getHeader(SEC_FETCH_MODE_HEADER);
56+
String dest = request.getHeader(SEC_FETCH_DEST_HEADER);
57+
58+
boolean isSimpleTopLevelNavigation = MODE_NAVIGATE.equals(mode) || "GET".equals(request.getMethod());
59+
boolean isNotObjectOrEmbedRequest = !DEST_EMBED.equals(dest) && !DEST_OBJECT.equals(dest);
60+
61+
return isSimpleTopLevelNavigation && isNotObjectOrEmbedRequest;
62+
}
63+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.struts2.result;
20+
21+
import com.opensymphony.xwork2.ActionInvocation;
22+
import com.opensymphony.xwork2.Result;
23+
import org.apache.logging.log4j.LogManager;
24+
import org.apache.logging.log4j.Logger;
25+
import org.apache.logging.log4j.message.ParameterizedMessage;
26+
import org.apache.struts2.StrutsException;
27+
import org.apache.struts2.result.plain.HttpHeader;
28+
import org.apache.struts2.result.plain.ResponseBuilder;
29+
30+
import javax.servlet.http.Cookie;
31+
import javax.servlet.http.HttpServletResponse;
32+
33+
/**
34+
* This result can only be used in code, as a result of action's method, eg.:
35+
* <p>
36+
* public PlainResult execute() {
37+
* return response -> response.write("");
38+
* }
39+
* <p>
40+
* Please notice the result type of the method is a PlainResult not a String.
41+
*/
42+
public interface PlainResult extends Result {
43+
44+
Logger LOG = LogManager.getLogger(PlainResult.class);
45+
46+
@Override
47+
default void execute(ActionInvocation invocation) throws Exception {
48+
LOG.debug("Executing plain result");
49+
ResponseBuilder builder = new ResponseBuilder();
50+
write(builder);
51+
52+
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
53+
54+
if (response.isCommitted()) {
55+
if (ignoreCommitted()) {
56+
LOG.warn("Http response already committed, ignoring & skipping!");
57+
return;
58+
} else {
59+
throw new StrutsException("Http response already committed, cannot modify it!");
60+
}
61+
}
62+
63+
for (HttpHeader<String> header : builder.getStringHeaders()) {
64+
LOG.debug(new ParameterizedMessage("A string header: {} = {}", header.getName(), header.getValue()));
65+
response.addHeader(header.getName(), header.getValue());
66+
}
67+
for (HttpHeader<Long> header : builder.getDateHeaders()) {
68+
LOG.debug(new ParameterizedMessage("A date header: {} = {}", header.getName(), header.getValue()));
69+
response.addDateHeader(header.getName(), header.getValue());
70+
}
71+
for (HttpHeader<Integer> header : builder.getIntHeaders()) {
72+
LOG.debug(new ParameterizedMessage("An int header: {} = {}", header.getName(), header.getValue()));
73+
response.addIntHeader(header.getName(), header.getValue());
74+
}
75+
76+
for (Cookie cookie : builder.getCookies()) {
77+
LOG.debug(new ParameterizedMessage("A cookie: {} = {}", cookie.getName(), cookie.getValue()));
78+
response.addCookie(cookie);
79+
}
80+
81+
response.getWriter().write(builder.getBody());
82+
response.flushBuffer();
83+
}
84+
85+
/**
86+
* Implement this method in action using lambdas
87+
*
88+
* @param response a response builder used to build a Http response
89+
*/
90+
void write(ResponseBuilder response);
91+
92+
/**
93+
* Controls if result should ignore already committed Http response
94+
* If set to true only a warning will be issued and the rest of the result
95+
* will be skipped
96+
*
97+
* @return boolean false by default which means an exception will be thrown
98+
*/
99+
default boolean ignoreCommitted() {
100+
return false;
101+
}
102+
103+
}
104+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.struts2.result.plain;
20+
21+
import java.io.StringWriter;
22+
23+
class BodyWriter {
24+
25+
private final StringWriter body = new StringWriter();
26+
27+
public BodyWriter write(String out) {
28+
body.write(out);
29+
return this;
30+
}
31+
32+
public BodyWriter writeLine(String out) {
33+
body.write(out);
34+
body.write("\n");
35+
return this;
36+
}
37+
38+
public String getBody() {
39+
return body.toString();
40+
}
41+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.struts2.result.plain;
20+
21+
class DateHttpHeader implements HttpHeader<Long> {
22+
23+
private final String name;
24+
private final Long value;
25+
26+
public DateHttpHeader(String name, Long value) {
27+
this.name = name;
28+
this.value = value;
29+
}
30+
31+
public String getName() {
32+
return name;
33+
}
34+
35+
public Long getValue() {
36+
return value;
37+
}
38+
}

0 commit comments

Comments
 (0)