# 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 : * Source : https://developer.mozilla.org/en-US/docs/HTTP_access_control#Preflighted_requests * Google cache : http://webcache.googleusercontent.com/search?q=cache:f1R5zF__S20J:https://developer.mozilla.org/en-US/docs/HTTP_access_control+cors%2Bmozilla&cd=1&hl=en&ct=clnk&gl=fr # 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 */ @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; /* 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 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 retrieveCustomHeaders(HttpServletRequest httpRequest) { List xHeaders = new ArrayList(); String name = null; if (httpRequest == null) { throw new IllegalArgumentException("HTTP Request cannot be null !"); } Enumeration 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 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 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[] 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 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; } {{Note | : les outils d'audit de W3AF (http://w3af.sourceforge.net) contiennent des plugins pour automatiquement auditer l'application web pour vérifier s'ils exécutent ce type de contre-mesure.}} 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