CORS OriginHeaderScrutiny

From OWASP
Jump to: navigation, search

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

Contents

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.<br/>
 * 
 * This implementation has a dependency on EHCache API because<br/>
 * 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<String> allowedDomains = new ArrayList<String>();

	/**
	 * {@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<String> 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<String> enumAsList(Enumeration<String> 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