CORS OriginHeaderScrutiny

Last revision (mm/dd/yy): 08/16/2013

Introduction
CORS stands for Cross-Origin Resource Sharing.

Is a feature offering the possbility for:
 * A web application to expose resources to all or restricted domain,
 * A web client to make AJAX request for resource on other domain than is source domain.

This article will focus on role of the Origin header in exchange between web client and web application.

The basic process is composed by steps below (sample HTTP resquest/response has been taken from Mozilla Wiki):


 * Step 1 : Web client send request to get resource from a different domain.

GET /resources/public-data/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive Referer: http://foo.example/examples/access-control/simpleXSInvocation.html Origin: http://foo.example

[Request Body]

The web client inform is source domain using the HTTP request header "Origin".


 * Step 2 : Web application respond to request.

HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 00:23:53 GMT Server: Apache/2.0.61 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: application/xml Access-Control-Allow-Origin: *

[Response Body]

The web application informs web client of the allowed domain using the HTTP response header Access-Control-Allow-Origin. The header can contains a '*' to indicate that all domain are allowed OR a specified domain to indicate the specified allowed domain.


 * Step 3 : Web client process web application response.

According to the CORS W3C specification, it's up to the web client (usually a browser) to determine, using the web application response HTTP header Access-Control-Allow-Origin, if the web client is allowed to access response data.

Risk
A reminder : Into this article we focus on web application side because it's the only part in which we have the maximum of control.

The risk here is that a web client can put any value into the Origin request HTTP header in order to force web application to provide it the target resource content. In the case of a Browser web client, the header value is managed by the browser but another "web client" can be used (like Curl/Wget/Burp suite/...) to change/override the "Origin" header value...

Countermeasure
Option A: Use CORS authenticated request

In this option, we enable authentication on the resources accessed and require that the user/application credentials be passed with the CORS requests.

If the CORS resources exposed are classified as sensitive (and CORS exposition is mandatory) it's a good option but if the objective is only to ensure that the request originator is really one of the allowed (to avoid rogue call), there somes drawback with this option, among others:
 * The target application must manage users (or applications) credentials repositories including features like password expiry, password reset, brute force prevention, account lock/unlock,...
 * The client application must store (in a secure way) the credentials to use,
 * The client application must manage/configure credentials transfer in HTTP request in order that credentials are send only in case of CORS requests to target application.

Option B: We can scrutiny the Origin header value on server side

In this option, the objective is to work between the step 1 and 2 of the CORS HTTP requests/responses exchange process (see above).

To achieve it, we will use JEE Web Filter that will ensure the following points for each incoming HTTP CORS requests:
 * 1) Have only one and non empty instance of the origin header,
 * 2) Have only one and non empty instance of the host header,
 * 3) The value of the origin header is present in a internal allowed domains list (white list). As we act before the step 2 of the CORS HTTP requests/responses exchange process, allowed domains list is yet provided to client,
 * 4) Cache IP of the sender for 1 hour. If the sender send one time a origin domain that is not in the white list then all is requests will return an HTTP 403 response (protract allowed domain guessing).

We use the method above because it's not possible to identify up to 100% that the request come from one expected client application, since:
 * All information of a HTTP request can be faked,
 * It's the browser (or others tools) that send the HTTP request then the IP address that we have access to is the client IP address.

In a Enterprise inter application communication it's possible to add a check on the client IP range. "Business to business" communication offer possibility to for each part to indicate to others parts the IP range that it will be used by its applications.

Sample implementation: Filter class

import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; 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 CORS "Origin" HTTP header. * * This implementation has a dependency on EHCache API because * it use Caching for blacklisted client IP in order to enhance performance. * * Assume here that all CORS resources are grouped in context path "/cors/". * */ @WebFilter("/cors/*") public class CORSOriginHeaderScrutiny implements Filter {

/** Filter configuration */ @SuppressWarnings("unused") private FilterConfig filterConfig = null;

/** Cache used to cache blacklisted Clients (request sender) IP address */ private Cache blackListedClientIPCache = null;

/** Domains allowed to access to resources (white list) */ private List allowedDomains = new ArrayList;

/**	 * {@inheritDoc} * 	 * @see Filter#init(FilterConfig) */	@Override public void init(FilterConfig fConfig) throws ServletException { // Get filter configuration this.filterConfig = fConfig; // Initialize Client IP address dedicated cache with a cache of 60 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(3600) .diskExpiryThreadIntervalSeconds(450) .persistence(cachePersistence) .maxEntriesLocalHeap(10000) .logging(false); cacheConfig.setName("BlackListedClientsCacheConfig"); this.blackListedClientIPCache = new Cache(cacheConfig); this.blackListedClientIPCache.setName("BlackListedClientsCache"); CacheManager.getInstance.addCache(this.blackListedClientIPCache); // Load domains allowed white list (hard coded here only for example) this.allowedDomains.add("http://www.html5rocks.com"); this.allowedDomains.add("https://www.mydomains.com"); }

/**	 * {@inheritDoc} * 	 * @see Filter#destroy */	@Override public void destroy { // Remove Cache CacheManager.getInstance.removeCache("BlackListedClientsCache"); }

/**	 * {@inheritDoc} * 	 * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain) */	@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = ((HttpServletRequest) request); HttpServletResponse httpResponse = ((HttpServletResponse) response); List headers = null; boolean isValid = false; String origin = null; String clientIP = httpRequest.getRemoteAddr;

/* Step 0 : Check presence of client IP in black list */ if (this.blackListedClientIPCache.isKeyInCache(clientIP)) { // Return HTTP Error without any information about cause of the request reject ! httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN); // Add trace here // ....			// Quick Exit return; }

/* Step 1 : Check that we have only one and non empty instance of the "Origin" header */ headers = CORSOriginHeaderScrutiny.enumAsList(httpRequest.getHeaders("Origin")); if ((headers == null) || (headers.size != 1)) { // If we reach this point it means that we have multiple instance of the "Origin" header // Add client IP address to black listed client addClientToBlacklist(clientIP); // Return HTTP Error without any information about cause of the request reject ! httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN); // Add trace here // ....			// Quick Exit return; }		origin = headers.get(0);

/* Step 2 : Check that we have only one and non empty instance of the "Host" header */ headers = CORSOriginHeaderScrutiny.enumAsList(httpRequest.getHeaders("Host")); if ((headers == null) || (headers.size != 1)) { // If we reach this point it means that we have multiple instance of the "Host" header // Add client IP address to black listed client addClientToBlacklist(clientIP); // Return HTTP Error without any information about cause of the request reject ! httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN); // Add trace here // ....			// Quick Exit return; }

/* Step 3 : Perform analysis - Origin header is required */ if ((origin != null) && !"".equals(origin.trim)) { if (this.allowedDomains.contains(origin)) { // Check if origin is in allowed domain isValid = true; } else { // Add client IP address to black listed client addClientToBlacklist(clientIP); isValid = false; // Add trace here // ....			}		}

/* Step 4 : Finalize request next step */ if (isValid) { // Analysis OK then pass the request along the filter chain chain.doFilter(request, response); } else { // Return HTTP Error without any information about cause of the request reject ! httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN); }	}

/**	 * Blacklist client * 	 * @param clientIP Client IP address */	private void addClientToBlacklist(String clientIP) { // Add client IP address to black listed client Element cacheElement = new Element(clientIP, clientIP); this.blackListedClientIPCache.put(cacheElement); }

/**	 * Convert a enumeration to a list * 	 * @param tmpEnum Enumeration to convert * @return list of string or null is input enumeration is null */	private static List enumAsList(Enumeration tmpEnum) { if (tmpEnum != null) { return Collections.list(tmpEnum); }		return null; } }

Note: W3AF audit tools (http://w3af.org) contains plugins to automatically audit web application to check if they implements this type of countermeasure.

 It's very useful to include this type of tools into a web application development process in order to perform a regular automatic first level check (do not replace an manual audit and manual audit must be also conducted regularly).

Informations links

 * 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
 * CORS Abuse : http://blog.secureideas.com/2013/02/grab-cors-light.html