CORS OriginHeaderScrutiny

Last revision (mm/dd/yy): 10/12/2012

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):

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
 * Step 1 : Web client send request to get resource from a different domain.

[Request Body] The web client inform is source domain using the HTTP request header "Origin".

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: *
 * Step 2 : Web application respond to request.

[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.

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....
 * Step 3 : Web client process web application response.

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

The risk here 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 the computer can contains malware or another web client can be used like Curl,OWASP Zap Proxy,....

Countermeasure
We must scrutiny the Origin value on server side.

To achieve it we will use JEE Web Filter that will ensure that the domain received is consistent with the HTTP request domain source.

Sample implementation: Filter class import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Enumeration;

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 reverse DNS resolving result 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 Domain's resolved IP address private Cache domainsIPCache = null;

/**	 * {@inheritDoc} * 	 * @see Filter#init(FilterConfig) */	@Override public void init(FilterConfig fConfig) throws ServletException { // Get filter configuration this.filterConfig = fConfig; // Initialize Domain IP address dedicated cache with a cache of 15 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(900).statistics(false) .diskExpiryThreadIntervalSeconds(450).persistence(cachePersistence).maxEntriesLocalHeap(10000).logging(false); cacheConfig.setName("DomainsCacheConfig"); this.domainsIPCache = new Cache(cacheConfig); this.domainsIPCache.setName("DomainsCache"); CacheManager.getInstance.addCache(this.domainsIPCache); }

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

/**	 * {@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); Enumeration headerNames = httpRequest.getHeaderNames; boolean isValid = false; String origin = null; String headerName = null; String domainIP = null;

/* Step 1 : Retrieve the value of the "Origin" HTTP request header */ while (headerNames.hasMoreElements) { headerName = headerNames.nextElement; if ("origin".equalsIgnoreCase(headerName)) { origin = httpRequest.getHeader(headerName); break; }		}

/* Step 2 : Perform analysis */ // Origin header is required if ((origin != null) && !"".equals(origin.trim)) { try { // Remove HTTP / HTTPS protocols origin = origin.toLowerCase; origin = origin.replaceFirst("http://", ""); origin = origin.replaceFirst("https://", "");

// Get IP address of the specified domain if (this.domainsIPCache.get(origin) != null) { // First using Cache domainIP = (String) this.domainsIPCache.get(origin).getValue; } else { // Second using reverse DNS and update Cache InetAddress clientAddress = InetAddress.getByName(origin); if (clientAddress != null) { domainIP = clientAddress.getHostAddress; Element cacheElement = new Element(origin, domainIP); this.domainsIPCache.put(cacheElement); }				}

// Compare IP addresses : Specified domain IP address against HTTP request sender IP address if ((domainIP != null) && domainIP.equals(httpRequest.getRemoteAddr)) { isValid = true; }			}			catch (UnknownHostException uhe) { // We print stack trace here for sample but in real app. a notification must be sent // to monitoring system in order to log malicious request... uhe.printStackTrace; }		}

// 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); }	}

}

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