(OWASP汉化)攻击系列大全(十四):跨源资源共享原始标头审查

最新版本 (mm/dd/yy): 08/16/2013

翻译自CORS OriginHeaderScrutiny

漏洞描述

CORS表示跨源资源共享,它作为一种功能提供了以下可能性

  • 将资源公开给所有或受限域的Web应用程序
  • 一个Web客户端,用于对源域以外的其他域的资源进行AJAX请求

本文将重点讨论原始标头在Web客户端和Web应用程序之间交换的作用。
基本过程由以下步骤组成(从Mozilla Wiki发出的简单HTTP请求/响应)

第一步:web客户端发送请求来从其他域获取资源

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]

Web客户端通知使用HTTP请求头“Origin”的源域

第二步:web应用程序响应请求

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]

Web应用程序使用HTTP响应标头Access-Control-Allow-Origin通知Web客户端允许的域。该标头可以通过包含一个’*’来表示所有的域都被允许或者一个指定的域来指示指定的允许域。

第三步:web客户端执行web应用程序的响应

根据CORS W3C规范,如果允许Web客户端访问响应数据,则Web客户端(通常是浏览器)使用Web应用程序响应的HTTP标头Access-Control-Allow-Origin来确定。

风险

提醒:在本文中我们将重点放在Web应用程序端,因为它是我们控制最多的唯一部分。
风险在于web应用端可以在源HTTP请求头中放入任何值来强制web应用端提供目标资源的内容。在浏览器为Web客户端的情况下,标头的值不仅可以由浏览器提供,更可以被其他”web客户端”(类似Curl\Wget\Burp suite..)来修改、覆写”Origin”字段的值…

对策

对策A:使用CORS请求认证

在此选项中,我们对所访问的资源启用身份验证,并要求将用户/应用程序凭证与CORS请求一起传递。
如果暴露的CORS资源被分类为敏感(CORS的暴露是强制性的),这是一个很好的选择,但如果目的只是为了确保请求发起者确实是允许的(避免恶意请求)之一,那么该选项有一些缺陷,其中包括:
– 目标应用程序必须管理用户(或者应用)的凭证储存库,包括密码有效期,密码重置,暴力破解防护,账户锁定/解锁等功能
– 客户端必须存储(以一种安全的方式)凭证来使用
– 客户端应用程序必须在HTTP请求中管理/配置证书传输,以便只有在CORS请求发送到目标应用程序的情况下才会发送证书。

对策B:我们可以在服务端审查源标头的值

在这个选项中,目标是在CORS HTTP请求/响应交换过程的第1步和第2步之间进行操作(见上文)。
为了达成目的,我们将使用JEE web Filter来确保接下来传入的HTTP CROS请求具有如下特点
– 只有一个非空的原始标头实例
– 只有一个非空的host标头实例
– 原始标头的值存在于允许的内部域列表(白名单)中。 当我们在CORS HTTP请求/响应交换过程的第2步之前采取行动时,允许的域列表尚未提供给客户端
– 缓存发送者IP一个小时、如果发送者发送了一次不在白名单中的原始域,则对所有请求将返回一个HTTP 403响应(拖延将导致域名猜测)

即使我们使用上面的方法,也不可能100%确定请求来自一个预期的客户端应用程序,原因是
– HTTP请求的所有信息都可被伪造
– 浏览器(或其他工具)发送HTTP请求,然后我们有权访问客户端IP地址。

在企业内部应用程序通信中,可以在客户端IP范围上添加检查。“企业对企业”通信为每个部分提供了向其他部分指示其应用程序将使用的IP范围的可能性。

简单示例:过滤器实现

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;

/**
 * 将样例过滤器实现用于审查CORS“Origin”HTTP标头.<br/>
 * 
 * 这个实现依赖于EHCache API,因为<br/>
 * 它将缓存用于列入黑名单的客户端IP,以提高性能.
 * 
 * 这里假设所有的CORS资源都按上下文路径分组 "/cors/".
 * 
 */
@WebFilter("/cors/*")
public class CORSOriginHeaderScrutiny implements Filter {

    /** 过滤器配置 */
    @SuppressWarnings("unused")
    private FilterConfig filterConfig = null;

    /** 用于缓存列入黑名单的客户端(请求发件人)IP地址的缓存 */
    private Cache blackListedClientIPCache = null;

    /** 允许域访问资源(白名单) */
    private List<String> allowedDomains = new ArrayList<String>();

    /**
     * {@inheritDoc}
     * 
     * @see Filter#init(FilterConfig)
     */
    @Override
    public void init(FilterConfig fConfig) throws ServletException {
        // 获取过滤器配置
        this.filterConfig = fConfig;
        // 初始化客户端IP地址专用高速缓存,每个项目的高速缓存60分钟过期
        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);
        // 加载域允许使用白名单(例如,在这里硬编码)
        this.allowedDomains.add("http://www.html5rocks.com");
        this.allowedDomains.add("https://www.mydomains.com");
    }

    /**
     * {@inheritDoc}
     * 
     * @see Filter#destroy()
     */
    @Override
    public void destroy() {
        // 移除缓存
        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();

        /* 步骤 0 : 检查黑名单中是否存在客户端IP */
        if (this.blackListedClientIPCache.isKeyInCache(clientIP)) {
            // 返回HTTP错误,没有任何有关请求拒绝原因的信息!
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
            // 在这里添加跟踪
            // ....
            // 快速退出
            return;
        }

        /* 步骤 1 : 检查我们是否只有一个“Origin”标头的非空实例 */
        headers = CORSOriginHeaderScrutiny.enumAsList(httpRequest.getHeaders("Origin"));
        if ((headers == null) || (headers.size() != 1)) {
            // 如果我们运行到这,这意味着我们的“Origin”标头有多个实例
            // 将客户端IP地址添加到黑名单
            addClientToBlacklist(clientIP);
            // 返回HTTP错误,没有任何有关请求拒绝原因的信息!
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
            // 在这里添加跟踪
            // ....
            // 快速退出
            return;
        }
        origin = headers.get(0);

        /* 步骤 2 : 检查我们是否只有一个“Host”标头的非空实例 */
        headers = CORSOriginHeaderScrutiny.enumAsList(httpRequest.getHeaders("Host"));
        if ((headers == null) || (headers.size() != 1)) {
            // 如果我们运行到这,这意味着我们的“Host”标头有多个实例
            // 将客户端IP地址添加到黑名单
            addClientToBlacklist(clientIP);
            // 返回HTTP错误,没有任何有关请求拒绝原因的信息
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
            // 在这里添加跟踪
            // ....
            // 快速退出
            return;
        }

        /* 步骤 3 : 执行分析 - 需要Origin标头 */
        if ((origin != null) && !"".equals(origin.trim())) {
            if (this.allowedDomains.contains(origin)) {
                // 检查来源是否在允许的域中
                isValid = true;
            } else {
                // 将客户端IP地址添加到黑名单
                addClientToBlacklist(clientIP);
                isValid = false;
                // 在这里添加跟踪
                // ....
            }
        }

        /* 步骤 4 : 完成下一步请求 */
        if (isValid) {
            // 分析完成,然后沿着过滤器链传递请求
            chain.doFilter(request, response);
        } else {
            // 返回HTTP错误,没有任何有关请求拒绝原因的信息!
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
        }
    }

    /**
     * 客户端黑名单
     * 
     * @param clientIP Client IP address
     */
    private void addClientToBlacklist(String clientIP) {
        // 将客户端IP地址添加到黑名单
        Element cacheElement = new Element(clientIP, clientIP);
        this.blackListedClientIPCache.put(cacheElement);
    }

    /**
     * 将枚举转换为列表
     * 
     * @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;
    }
}

注意:W3AF审计工具(http://w3af.org)包含了自动审核Web应用程序的插件,以检查应用端是否实现了这种对策。将这种类型的工具纳入Web应用程序开发流程以便执行定期的自动一级检查(不要取代手动审计,而且必须定期执行手动审计)是非常有用的做法。

相关链接