Skip to content

Commit

Permalink
Fix CORS headers (frankframework#5257)
Browse files Browse the repository at this point in the history
  • Loading branch information
nielsm5 authored Aug 30, 2023
1 parent 7f86c1d commit 0c69d02
Show file tree
Hide file tree
Showing 8 changed files with 421 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,14 @@
package nl.nn.adapterframework.management.web;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

import org.apache.commons.lang3.StringUtils;
import org.apache.cxf.Bus;
import org.apache.cxf.transport.servlet.CXFServlet;
import org.apache.logging.log4j.LogManager;
Expand All @@ -37,7 +34,6 @@

import nl.nn.adapterframework.lifecycle.DynamicRegistration;
import nl.nn.adapterframework.util.HttpUtils;
import nl.nn.adapterframework.util.StringUtil;

/**
* Main dispatcher for all API resources.
Expand All @@ -50,24 +46,12 @@ public class ServletDispatcher extends CXFServlet implements DynamicRegistration

private static final long serialVersionUID = 3L;

private Logger secLog = LogManager.getLogger("SEC");
private Logger log = LogManager.getLogger(this);
private final Logger secLog = LogManager.getLogger("SEC");
private final Logger log = LogManager.getLogger(this);

@Value("${iaf-api.enabled:true}")
private boolean isEnabled;

@Value("${iaf-api.cors.allowOrigin:''}")
private String allowedCorsOrigins; //Defaults to nothing

@Value("${iaf-api.cors.exposeHeaders:Allow, ETag, Content-Disposition}")
private String exposedCorsHeaders;

//TODO: Maybe filter out the methods that are not present on the resource? Till then allow all methods
@Value("${iaf-api.cors.allowMethods:GET, POST, PUT, DELETE, OPTIONS, HEAD}")
private String allowedCorsMethods;

private List<String> allowedCorsDomains = new ArrayList<>();

@Override
public void init(ServletConfig servletConfig) throws ServletException {
if(!isEnabled) {
Expand All @@ -76,21 +60,6 @@ public void init(ServletConfig servletConfig) throws ServletException {

log.debug("initialize {} servlet", this::getName);
super.init(servletConfig);

if(StringUtils.isNotEmpty(allowedCorsOrigins)) {
List<String> allowedOrigins = StringUtil.split(allowedCorsOrigins);
for (String domain : allowedOrigins) {
if(domain.startsWith("http://")) {
log.warn("cross side resource domain ["+domain+"] is insecure, it is strongly encouraged to use a secure protocol (HTTPS)");
}
if(!domain.startsWith("http://") && !domain.startsWith("https://")) {
log.error("skipping invalid domain ["+domain+"], domains must start with http(s)://");
continue;
}
allowedCorsDomains.add(domain);
log.debug("whitelisted CORS domain ["+domain+"]");
}
}
}

@Override
Expand All @@ -105,57 +74,18 @@ public void invoke(HttpServletRequest request, HttpServletResponse response) thr
return;
}

final String method = request.getMethod();

/**
* Log POST, PUT and DELETE requests
*/
final String method = request.getMethod();
if(!method.equalsIgnoreCase("GET") && !method.equalsIgnoreCase("OPTIONS")) {
secLog.info(HttpUtils.getExtendedCommandIssuedBy(request));
}

/**
* Handle Cross-Origin Resource Sharing
*/
String origin = request.getHeader("Origin");
if (origin == null) {
// Return standard response if OPTIONS request w/o Origin header
if(method.equals("OPTIONS")) {
response.setHeader("Allow", allowedCorsMethods);
response.setStatus(200);
return;
}
}
else {
//Always add the cors headers when the origin has been set
if(allowedCorsDomains.contains(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);

String requestHeaders = request.getHeader("Access-Control-Request-Headers");
if (requestHeaders != null)
response.setHeader("Access-Control-Allow-Headers", requestHeaders);

response.setHeader("Access-Control-Expose-Headers", exposedCorsHeaders);
response.setHeader("Access-Control-Allow-Methods", allowedCorsMethods);

// Allow caching cross-domain permission
response.setHeader("Access-Control-Max-Age", "3600");
}
else {
//If origin has been set, but has not been whitelisted, report the request in security log.
secLog.info("host["+request.getRemoteHost()+"] tried to access uri["+request.getPathInfo()+"] with origin["+origin+"] but was blocked due to CORS restrictions");
}
//If we pass one of the valid domains, it can be used to spoof the connection
}

//TODO add X-Rate-Limit to prevent possible clients to flood the IAF API

/**
* Pass request down the chain, except for OPTIONS
*/
if (!method.equals("OPTIONS")) {
super.invoke(request, response);
}
super.invoke(request, response);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
Copyright 2022-2023 WeAreFrank!
Licensed 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 nl.nn.adapterframework.web.filters;

import java.io.IOException;
import java.util.List;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsUtils;

import nl.nn.adapterframework.util.StringUtil;

public class CorsFilter implements Filter {
private final Logger secLog = LogManager.getLogger("SEC");
private final Logger log = LogManager.getLogger(this);

@Value("${iaf-api.cors.allowOrigin:*}")
private String allowedCorsOrigins; //Defaults to ALL allowed

@Value("${iaf-api.cors.exposeHeaders:Allow, ETag, Content-Disposition}")
private String exposedCorsHeaders;

//TODO: Maybe filter out the methods that are not present on the resource? Till then allow all methods
@Value("${iaf-api.cors.allowMethods:GET, POST, PUT, DELETE, OPTIONS, HEAD}")
private String allowedCorsMethods;

@Value("${iaf-api.cors.enforced:false}")
private boolean enforceCORS;

private CorsConfiguration config = new CorsConfiguration();

@Override
public void init(FilterConfig filterConfig) throws ServletException {
List<String> allowedOrigins = StringUtil.split(allowedCorsOrigins);
for (String domain : allowedOrigins) {
if("*".equals(domain) || !domain.contains("*")) {
config.addAllowedOrigin(domain);
} else {
config.addAllowedOriginPattern(domain);
}
}
log.debug("whitelisted CORS origins: {} and patterns: {}", config::getAllowedOrigins, config::getAllowedOriginPatterns);
}

@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;

/**
* Handle Cross-Origin Resource Sharing
*/
if(enforceCORS && CorsUtils.isCorsRequest(request)) {
String originHeader = request.getHeader("Origin");
String origin = config.checkOrigin(originHeader);
if (origin != null) {
response.setHeader("Access-Control-Allow-Origin", origin);

String requestHeaders = request.getHeader("Access-Control-Request-Headers");
if (requestHeaders != null)
response.setHeader("Access-Control-Allow-Headers", requestHeaders);

response.setHeader("Access-Control-Expose-Headers", exposedCorsHeaders);
response.setHeader("Access-Control-Allow-Methods", allowedCorsMethods);

// Allow caching cross-domain permission
response.setHeader("Access-Control-Max-Age", "3600");
} else {
//If origin has been set, but has not been whitelisted, report the request in security log.
secLog.info("host["+request.getRemoteHost()+"] tried to access uri["+request.getPathInfo()+"] with origin["+originHeader+"] but was blocked due to CORS restrictions");
log.warn("blocked request with origin [{}]", originHeader);
response.setStatus(400);
return; //actually block the request
}
//If we pass one of the valid domains, it can be used to spoof the connection
}

/**
* Pass request down the chain, except for OPTIONS
*/
// Return standard response if OPTIONS request w/o Origin header
if(CorsUtils.isPreFlightRequest(request)) {
response.setHeader("Allow", allowedCorsMethods);
response.setStatus(200);
} else {
chain.doFilter(request, response);
}
}

@Override
public void destroy() {
// nothing to destroy
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
Copyright 2023 WeAreFrank!
Licensed 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 nl.nn.adapterframework.web.filters;

import java.util.EnumSet;

import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import nl.nn.adapterframework.util.SpringUtils;

@WebListener
public class CorsFilterConfigurer implements ServletContextListener {
private Logger log = LogManager.getLogger(this);

@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();

try {
Dynamic filter = createFilter(context);

String[] urlMapping = new String[] {"/iaf/api/*"};
filter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, urlMapping);
} catch (Exception e) {
log.fatal("unable to create CORS filter", e);
context.log("unable to create CORS filter", e);
}
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
// nothing to destroy
}

private Dynamic createFilter(ServletContext context) {
WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(context);
if(wac != null) {
log.info("creating CorsFilter through Application Context [{}]", wac::getDisplayName);
CorsFilter filterInstance = SpringUtils.createBean(wac, CorsFilter.class);
return context.addFilter("CorsFilter", filterInstance);
} else {
log.info("creating CorsFilter without Application Context");
return context.addFilter("CorsFilter", CorsFilter.class);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
package nl.nn.adapterframework.webcontrol;
package nl.nn.adapterframework.web.filters;

import java.io.IOException;
import java.util.LinkedList;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
package nl.nn.adapterframework.webcontrol;
package nl.nn.adapterframework.web.filters;

import java.util.EnumSet;

Expand Down
Loading

0 comments on commit 0c69d02

Please sign in to comment.