CORS OriginHeaderScrutiny

From OWASP
Revision as of 15:38, 25 September 2012 by Dominique RIGHETTO (Talk | contribs)

Jump to: navigation, search

Last revision (mm/dd/yy): 09/25/2012

Contents

Introduction

CORS stands for Cross-Origin Resource Sharing.


Is an feature offering the possbility to:

  • A web application to expose resources to all or restricted domain,
  • A web client to made 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 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:

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.concurrent.TimeUnit;
import javax.cache.Cache;
import javax.cache.CacheConfiguration.Duration;
import javax.cache.CacheConfiguration.ExpiryType;
import javax.cache.CacheManager;
import javax.cache.Caching;
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;

/**
 * Sample filter implementation to scrutiny CORS "Origin" HTTP header.<br/>
 * 
 * This implementation has a dependency on JSR107 (Java Cache API) because<br/>
 * 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/".
 * 
 * @see "http://gregluck.com/blog/archives/2011/07/start-using-jsr107s-jcache-api/"
 * @see "https://github.com/jsr107/jsr107spec"
 * @see "https://github.com/jsr107/RI"
 */
@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<Object, Object> 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
		CacheManager cacheManager = Caching.getCacheManager();
		Duration duration = new Duration(TimeUnit.MINUTES, 15);
		this.domainsIPCache = cacheManager.createCacheBuilder("DomainsCache").setExpiry(ExpiryType.ACCESSED, duration)
                                                                                     .setStatisticsEnabled(false).build();

	}

	/**
	 * {@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<String> 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.containsKey(origin)) {
					// First using Cache
					domainIP = (String) this.domainsIPCache.get(origin);
				} else {
					// Second using reverse DNS and update Cache
					InetAddress clientAddress = InetAddress.getByName(origin);
					if (clientAddress != null) {
						domainIP = clientAddress.getHostAddress();
						this.domainsIPCache.put(origin, domainIP);
					}
				}

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

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

}

Note: W3AF audit tools (http://w3af.sourceforge.net) contains plugins to automatically audit web application to check if they implement 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