Table of Contents
Introduction
CORS représente la Cross-Origin Resource Sharing.
C'est une caractéristique offrant la possibilité de :
Une application web pour exposer les ressources à tout le domaine ou restreint,
Un client web pour faire une requête AJAX pour la ressource sur d'autre domaine qu'est le domaine source.
Cet article se concentrera sur la requête HTTP Request Preflight proposée par la spécification CORS W3C et (surtout) la façon de configurer une protection, côté application web, contre la requête HTTP CORS qui tente de contourner le processus de contrôle en amont.
Présentation du processus Request preflight
Pour pas copier l'explication et parce que le wiki Mozilla a un grand article d'introduction à CORS, vous pouvez lire une description du processus en utilisant le lien ci-dessous :
Risque
Le Request preflight a comme objectif de garantir que la requête HTTP n'aura pas de mauvais impact sur les données, cela, en utilisant une première requête dans laquelle le navigateur décrit la requête HTTP finale qu'il enverra plus tard. Le risque principal ici (pour l'application web), est que le processus de Request preflight est entièrement dirigé côté client (par le navigateur) et ensuite n'importe quoi justifie l'application de web que la requête de Request preflight sera toujours suivie…
Un utilisateur peut créer/envoyer (en utilisant des outils comme Curl,OWASP Zap Proxy,…) une requête HTTP finale sans envoyer auparavant la première Request preflight et évite ensuite le processus de Request preflight pour agir sur les données d'une façon dangereuse.
Contre-mesure
Nous devons garantir l'acquiescement du processus Request Preflight process côté serveur. Pour l'accomplir nous utiliserons JEE Web Filter qui vérifiera chaque requête CORS utilsant ces étapes :
- Etape 1 : Déterminer le type de requête entrante,,
- Etape 2 : Le processus selon le type de la requête utilisant le cache temporaire afin de maintenir l'état de contrôle en amont des étapes du processus.
Implémentation : Filter class
import java.io.IOException;
import java.util.Collections;
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.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.config.CacheConfiguration;
import net.sf.ehcache.config.PersistenceConfiguration;
import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
/**
* Sample filter implementation to scrutiny that CORS "Request Preflight" process is followed by HTTP request concerned.
*
* This implementation has a dependency on EHCache API because it use Caching to store preflighted requests.
*
* Assume here that all CORS resources are grouped in context path "/cors/".
*
* @see "<__yamdwe_nowiki>0</__yamdwe_nowiki>
*/
@SuppressWarnings("static-method")
@WebFilter("/cors/*")
public class CORSRequestPreflightProcessScrutiny implements Filter {
// Filter configuration
private FilterConfig filterConfig = null;
// Period during which we keep a request as correctly followed the request preflight process
private int requestPreflightCacheDelayInSeconds = 60;
// Cache used to cache preflighted requests
private Cache requestPreflightCache = null;
/**
* {@inheritDoc}
*
* @see Filter#init(FilterConfig)
*/
@Override
public void init(FilterConfig fConfig) throws ServletException {
// Get filter configuration
this.filterConfig = fConfig;
// Initialize preflighted requests dedicated cache with a cache of X minutes expiration delay for each item
PersistenceConfiguration cachePersistence = new PersistenceConfiguration();
cachePersistence.strategy(PersistenceConfiguration.Strategy.NONE);
CacheConfiguration cacheConfig = new CacheConfiguration().memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.FIFO).eternal(false)
.timeToLiveSeconds(this.requestPreflightCacheDelayInSeconds)
.statistics(false).diskExpiryThreadIntervalSeconds(this.requestPreflightCacheDelayInSeconds / 2)
.persistence(cachePersistence).maxEntriesLocalHeap(10000).logging(false);
cacheConfig.setName("PreflightedRequestsCacheConfig");
this.requestPreflightCache = new Cache(cacheConfig);
this.requestPreflightCache.setName("PreflightedRequestsCache");
CacheManager.getInstance().addCache(this.requestPreflightCache);
}
/**
* {@inheritDoc}
*
* @see Filter#destroy()
*/
@Override
public void destroy() {
// Remove Cache
CacheManager.getInstance().removeCache("PreflightedRequestsCache");
}
/**
* {@inheritDoc}
*
* @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest htReq = ((HttpServletRequest) request);
HttpServletResponse htResp = ((HttpServletResponse) response);
int accessDeniedHttpResponse = HttpServletResponse.SC_FORBIDDEN;
String accessControlAllowMethods = "GET, POST";
String accessControlAllowOrigin = <__yamdwe_nowiki>1</__yamdwe_nowiki>;
/* Step 01 : Determine the type of the incoming request */
CORSRequestPreflightType corsReqType = determineRequestType(htReq);
/* Step 02 : Process request according to is type */
switch (corsReqType) {
// --HTTP request send by client to preflight a further 'Complex' request
case REQUEST_FOR_PREFLIGHT: {
CORSRequestPreflightData corsReq = new CORSRequestPreflightData(htReq);
// ----Step 2a: Check that request for preflight is valid, if not, send an access denied (do not give infos about bad resquest root cause)
if (corsReq.getOrigin().trim().isEmpty() || corsReq.getExpectedMethod().trim().isEmpty()) {
traceInvalidRequestDetected(htReq);
htResp.reset();
htResp.sendError(accessDeniedHttpResponse);
// Exit Filter : Use 'return' algorithm break in order to avoid multiple IF statement and enhance readability...
return;
}
// ----Step 2b: Store preflight request data in the Cache to keep (mark) the request as correctly followed the request preflight process
Element cachedRequest = new Element(CORSUtils.buildRequestCacheIdentifier(htReq), corsReq);
this.requestPreflightCache.put(cachedRequest);
// ----Step 2c: Return corresponding response - This part should be customized with application specific constraints.....
htResp.reset();
htResp.setStatus(HttpServletResponse.SC_OK);
htResp.setHeader("Access-Control-Allow-Origin", accessControlAllowOrigin);
htResp.setHeader("Access-Control-Allow-Methods", accessControlAllowMethods);
if (!corsReq.getExpectedCustomHeaders().isEmpty()) {
htResp.setHeader("Access-Control-Allow-Headers", corsReq.getExpectedCustomHeaders().toString().replaceFirst("\\[", "").replaceFirst("\\]", "").trim());
}
htResp.setIntHeader("Access-Control-Max-Age", (this.requestPreflightCacheDelayInSeconds));
break;
}
// --Normal HTTP request send by client that require preflight ie 'Complex' resquest in Preflight process
case COMPLEX_REQUEST: {
String rid = CORSUtils.buildRequestCacheIdentifier(htReq);
// ----Step 2a: Check if the current request has an entry into the preflighted requests Cache
if (this.requestPreflightCache.get(rid) == null) {
traceInvalidRequestDetected(htReq);
htResp.reset();
htResp.sendError(accessDeniedHttpResponse);
// Exit Filter : Use 'return' algorithm break in order to avoid multiple IF statement and enhance readability...
return;
}
// ----Step 2b: Check that preflight information declared during the preflight request match the current request on key information
CORSRequestPreflightData corsPreflightReq = (CORSRequestPreflightData) this.requestPreflightCache.get(rid).getValue();
String origin = CORSUtils.retrieveHeader("Origin", htReq);
List<String> customHeaders = CORSUtils.retrieveCustomHeaders(htReq);
boolean match = false;
// ------Start with comparison of "Origin" HTTP header (according to utility method impl. used to retrieve header reference cannot be null)...
if (origin.equals(corsPreflightReq.getOrigin())) {
// ------Continue with HTTP method...
if (accessControlAllowMethods.contains(htReq.getMethod()) && htReq.getMethod().equals(corsPreflightReq.getExpectedMethod())) {
// ------Finish with custom HTTP headers (use an method to avoid manual iteration on collection to increase the speed)...
if (customHeaders.size() == corsPreflightReq.getExpectedCustomHeaders().size()) {
Collections.sort(customHeaders);
Collections.sort(corsPreflightReq.getExpectedCustomHeaders());
if (customHeaders.toString().equals(corsPreflightReq.getExpectedCustomHeaders().toString())) {
match = true;
}
}
}
}
if (match) {
// Continue chain to next filter
chain.doFilter(request, response);
} else {
traceInvalidRequestDetected(htReq);
htResp.reset();
htResp.sendError(accessDeniedHttpResponse);
}
break;
}
// --Normal HTTP request send by client that do not require preflight ie 'Simple' resquest in Preflight process
case SIMPLE_REQUEST: {
// Continue chain to next filter
chain.doFilter(request, response);
break;
}
// --Unknown HTTP request type !
default: {
traceInvalidRequestDetected(htReq);
htResp.reset();
htResp.sendError(accessDeniedHttpResponse);
break;
}
}
}
/**
* Internal method to determine the type of the incoming request.
*
* @param htReq HTTP Request
* @return the type as enumeration item
*/
private CORSRequestPreflightType determineRequestType(HttpServletRequest htReq) {
CORSRequestPreflightType type = CORSRequestPreflightType.UNKNOWN;
if ("OPTIONS".equalsIgnoreCase(htReq.getMethod())) {
type = CORSRequestPreflightType.REQUEST_FOR_PREFLIGHT;
} else {
if (!CORSUtils.retrieveCustomHeaders(htReq).isEmpty()) {
type = CORSRequestPreflightType.COMPLEX_REQUEST;
} else if ("POST".equalsIgnoreCase(htReq.getMethod()) && !"application/x-www-form-urlencoded".equalsIgnoreCase(htReq.getContentType())
&& !"multipart/form-data".equalsIgnoreCase(htReq.getContentType()) && !"text/plain".equalsIgnoreCase(htReq.getContentType())) {
type = CORSRequestPreflightType.COMPLEX_REQUEST;
} else if ("HEAD".equalsIgnoreCase(htReq.getMethod()) || "DELETE".equalsIgnoreCase(htReq.getMethod()) || "PUT".equalsIgnoreCase(htReq.getMethod())
|| "TRACE".equalsIgnoreCase(htReq.getMethod()) || "CONNECT".equalsIgnoreCase(htReq.getMethod())) {
type = CORSRequestPreflightType.COMPLEX_REQUEST;
} else {
type = CORSRequestPreflightType.SIMPLE_REQUEST;
}
}
return type;
}
/**
* Method to add data of invalid request detected to a trace log
*
* @param htReq Invalid request detected
*/
private void traceInvalidRequestDetected(HttpServletRequest htReq) {
// Customize trace...
this.filterConfig.getServletContext().log("---[CORS Invalid request detected]---");
this.filterConfig.getServletContext().log(String.format("Client Address : %s", htReq.getRemoteAddr()));
this.filterConfig.getServletContext().log(String.format("Target URL : %s", htReq.getRequestURL()));
this.filterConfig.getServletContext().log(String.format("Query String : %s", htReq.getQueryString()));
this.filterConfig.getServletContext().log(String.format("HTTP Method : %s", htReq.getMethod()));
// Print more request useful data.....
this.filterConfig.getServletContext().log("-------------------------------------");
}
}
Implémentation : Utility class used by Filter import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
/**
* Utility class for CORS data retrieving & processing.
*/
public class CORSUtils {
/**
* Method to retrieve HTTP request custom headers list.
*
* @param httpRequest Source HTTP request
* @return List of custom headers (converted to uppercase to avoid case issue)
*/
public static List<String> retrieveCustomHeaders(HttpServletRequest httpRequest) {
List<String> xHeaders = new ArrayList<String>();
String name = null;
if (httpRequest == null) {
throw new IllegalArgumentException("HTTP Request cannot be null !");
}
Enumeration<String> headers = httpRequest.getHeaderNames();
while (headers.hasMoreElements()) {
name = headers.nextElement().toUpperCase().trim();
if (name.startsWith("X-")) {
xHeaders.add(name.trim());
}
}
return xHeaders;
}
/**
* Method to retrieve a HTTP Header value from the source HTTP request.
* Manage Header name case issue and take only first value.
*
* @param headerName HTTP name
* @param httpRequest Source HTTP request
* @return The HTTP Header value or "" if it cannot be found
*/
public static String retrieveHeader(String headerName, HttpServletRequest httpRequest) {
String value = "";
String name = null;
if (httpRequest == null) {
throw new IllegalArgumentException("HTTP Request cannot be null !");
}
if ((headerName == null) || headerName.trim().isEmpty()) {
throw new IllegalArgumentException("HTTP header name be null !");
}
Enumeration<String> headers = httpRequest.getHeaderNames();
while (headers.hasMoreElements()) {
name = headers.nextElement();
if (name.trim().equalsIgnoreCase(headerName)) {
value = httpRequest.getHeader(name);
break;
}
}
return value;
}
/**
* Method to build an identifier for a request into the preflighted requests cache
*
* @param httpRequest Source HTTP request
* @return The ID as String
*/
public static String buildRequestCacheIdentifier(HttpServletRequest httpRequest) {
return (httpRequest.getRemoteAddr() + "_" + httpRequest.getRequestURI()).trim();
}
}
Implémentation : Pojo class utilisée pour conserver l'information de la clé de requête de Preflight import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
/**
* Class to store information about a CORS preflighted request.
*/
@SuppressWarnings("serial")
public class CORSRequestPreflightData implements Serializable {
/** Final HTTP request expected method */
private String expectedMethod = null;
/** Final HTTP request expected custom headers */
private List<String> expectedCustomHeaders = null;
/** Current HTTP request uri */
private String uri = null;
/** Current HTTP request origin header */
private String origin = null;
/** Current Sender IP address */
private String sender = null;
/**
* Constructor
*
* @param httpRequest Source HTTP request
*/
public CORSRequestPreflightData(HttpServletRequest httpRequest) {
super();
String tmp = null;
if (httpRequest == null) {
throw new IllegalArgumentException("HTTP request cannot be null !");
}
this.sender = httpRequest.getRemoteAddr();
this.uri = httpRequest.getRequestURI();
this.origin = CORSUtils.retrieveHeader("Origin", httpRequest);
this.expectedMethod = CORSUtils.retrieveHeader("Access-Control-Request-Method", httpRequest);
tmp = CORSUtils.retrieveHeader("Access-Control-Request-Headers", httpRequest);
if (!tmp.trim().isEmpty()) {
this.expectedCustomHeaders = new ArrayList<String>();
String[] hs = tmp.split(",");
for (String h : hs) {
if ((h != null) && !h.trim().isEmpty()) {
this.expectedCustomHeaders.add(h.toUpperCase().trim());
}
}
}
}
/**
* Getter
*
* @return the expectedMethod
*/
public String getExpectedMethod() {
return this.expectedMethod;
}
/**
* Getter
*
* @return the expectedCustomHeaders
*/
public List<String> getExpectedCustomHeaders() {
return this.expectedCustomHeaders;
}
/**
* Getter
*
* @return the uri
*/
public String getUri() {
return this.uri;
}
/**
* Getter
*
* @return the origin
*/
public String getOrigin() {
return this.origin;
}
/**
* Getter
*
* @return the sender
*/
public String getSender() {
return this.sender;
}
}
Implémentation : Enumération utilisée pour représenter les differents types de requête CORS
/**
* Enumeration of the differents CORS "request preflight" HTTP request type.
*/
public enum CORSRequestPreflightType {
/** HTTP request send by client to preflight a further 'Complex' request */
REQUEST_FOR_PREFLIGHT,
/** Normal HTTP request send by client that require preflight ie 'Complex' resquest in Preflight process */
COMPLEX_REQUEST,
/** Normal HTTP request send by client that do not require preflight ie 'Simple' resquest in Preflight process */
SIMPLE_REQUEST,
/** Cannot determine request type */
UNKNOWN;
}
Il est très utile d'inclure ce type d'outils dans un processus de développement d'application web afin de procéder pèriodiquement à une vérification regulière de premier niveau (ne pas remplacer un audit manuel et un audit manuel doit également être effectué régulièrement).
Références
W3C Specification : http://www.w3.org/TR/cors/
Mozilla Wiki : https://developer.mozilla.org/en-US/docs/HTTP_access_control
Wikipedia : http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
