编码技巧——HTTP接口安全之CORS

日常开发中,对于一些新项目的api工程,会有专门的安全工程师对齐进行安全漏洞扫描,扫描出来的漏洞会被要求限期修复;本篇讲解CORS漏洞的基本概念、原理、示例,以及修复漏洞的代码示例;

1. 什么是CORS

跨域资源共享 (CORS) 是一种浏览器机制,可以对位于给定域之外的资源进行受控访问。它扩展并增加了同源策略 ( SOP ) 的灵活性。但是,如果网站的 CORS 策略配置和实施不当,它也会为基于跨域的攻击提供可能性。

根据以上的解释,知道:CORS不是漏洞,而是浏览器的一种机制;它是对跨域资源的受控访问,而不是禁止访问;由于概念涉及同源策略跨域,因此先简单介绍下相关概念;

2. 同源策略

SOP,同源策略 (Same Origin Policy),该策略是浏览器的一个安全基石;同源策略是浏览器的一个安全功能,不同源的数据禁止访问;如 lilnong.top 下的 ajax 访问 51vv.com 数据是会报错。(network 可以看到 response,证明限制是浏览器方的限制);

 如果没有同源策略,那么,你打开了一个合法网站,又打开了一个恶意网站。恶意网站的脚本能够随意的操作合法网站的任何可操作资源,没有任何限制。

浏览器的同源策略规定:不同域的客户端脚本没有明确授权的情况下,不能读写对方的资源。那么何为同源呢,即两个站点需要满足同协议,同域名,同端口这三个条件。

可以理解为同源策略的制定是从安全层面出发的,是对浏览器脚本的一种限制,目的是在用户使用浏览器时保护用户网络安全;但是,尽管同源策略SOP带来安全,同时也会带来一定程度的麻烦,因为有时候就是有跨域的需求,如随着Web应用的发展,网站由于自身业务的需求,需要实现一些跨域的功能,能够让不同域的页面之间能够相互访问各自页面的内容(如广告、物流等需要请求第三方的域名接口),即需要对一些指定的域名“跨域”

之所以会遇到跨域问题,正是因为 SOP 的各种限制。但是具体来说限制了什么呢?

其实 SOP 不是单一的定义,而是在不同情况下有不同的解释:

  • 限制 cookies、DOM 和 Javascript 的命名区域
  • 限制 iframe、图片等各种资源的内容操作
  • 限制 ajax 请求,准确来说是限制操作 ajax 响应结果,本质上跟上一条是一样的

下面是 3 个在实际应用中会遇到的例子:

  • 使用 ajax 请求其他跨域 API,最常见的情况,前端新手噩梦
  • iframe 与父页面交流,出现率比较低,而且解决方法也好懂
  • 对跨域图片(例如来源于 <img> )进行操作,在 canvas 操作图片的时候会遇到这个问题

SOP有多重要?如果没有了 SOP会发生什么:

  • 一个浏览器打开几个 tab,数据就泄露了
  • 你用 iframe 打开一个银行网站,你可以肆意读取网站的内容,就能获取用户输入的内容
  • 更加肆意地进行 CSRF

SOP同源策略是否总能起到限制跨域的作用呢?——不是!

表单提交、链接 这些项等同于切换页面 script标签的src、link标签的href、img标签的src、iframe标签的src 上述的资源可以引用,但是不可获取内容;

img 可以显示出来,但是你无法放入canvas二次使用,会把canvas的源污染。 iframe 可以显示,不可以获取DOM script 不可获取报错代码位置;

3. 跨域的示例

跨域原因说明示例
域名不同http://www.jd.com 与 http://www.taobao.com
域名相同,端口不同http://www.jd.com:8080 与 http://www.jd.com:8081
二级域名不同http://item.jd.com 与 http://miaosha.jd.com

如果域名和端口都相同,但是请求路径不同,不属于跨域,如:

跨域访问的一些场景

(1)在前后端分离的模式下,前后端的域名不一定是一致的,此时就会发生跨域访问的问题;

(2)电商网站想通过用户浏览器,通过发送ajax请求,加载第三方快递网站的物流信息;

(3)一级域名相同的子站域名希望调用主站域名的用户资料接口,并将数据显示出来;

通过上述的场景,我们发现有时候跨域不一定会有跨域问题,而是业务所需;因为跨域问题是浏览器对于ajax请求的一种安全限制:一个页面发起的ajax请求,只能是于当前页同域名的路径,这能有效的阻止跨站攻击。

因此,跨域问题,某种程度上可以理解为是针对ajax的一种限制;

但是这却给我们的开发带来了不变,而且在实际生产环境中,肯定会有很多台不同域名的服务器之间数据交互,域名地址和端口都可能不同,怎么办?下面会给出几种方式;在说明解决跨域问题的方案前,我们先做个小结:

同源策略SOP/跨域的总结:

(1) SOP是浏览器的一种安全机制,不满足同源的域名之间的资源访问属于"跨域访问",跨域访问会受到SOP的限制,称之为"跨域问题";

(2) 正因为SOP是浏览器的一种安全机制,而不受到服务端控制,即同源策略会阻止响应,但依然会发出请求。因为执行响应拦截的是浏览器而不是后端程序。事实上你的请求已经发到服务器并返回了结果,但是迫于安全策略,浏览器不允许你继续进行 js 操作,所以报出你熟悉的"blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.",因此——同源策略不能作为防范 CSRF 的方法,因为请求已经发出去了!

参考:前端网络安全必修 同源策略和CSRF - 掘金

4. 如何绕过跨域限制的方案

  • 1. nginx反向代理

思路是:利用nginx反向代理把跨域为不跨域,支持各种请求方式缺点是:需要在nginx进行额外配置,语义不清晰;

  • 2. CORS 规范化的跨域请求解决方案

优势:可在服务端进行控制是否允许跨域,可自定义规则,非常灵活;且支持各种请求方式;

CORS如何解决同源策略的限制?

CORS,跨域资源共享(Cross-origin resource sharing),是浏览器提供的一种机制,它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持;目前,所有浏览器都支持该功能,整个CORS通信过程,都是浏览器自动完成;而对于服务端,CORS通信与AJAX没有任何差别,因此不需要改变以前的业务逻辑,只不过,浏览器会在请求中携带一些头信息,而服务端需要以此判断是否运行其跨域然后在响应头中加入一些信息即可,一般通过写个Filter实现;下面介绍下CORS原理,通过原理我们就可以来写出一个CorsFilter了;

5. CORS原理

浏览器会将ajax请求分为两类,其处理方案略有差异:简单请求、特殊请求;这2种请求达到跨域条件的请求和服务端响应存在一些差异,这里分别介绍;

只要同时满足以下两大条件,就属于简单请求

1. 请求方法是以下三种方法之一:HEAD/GET/POST

2. HTTP的头信息不超出以下几种字段:Accept/Accept-Language/Content-Language/Last-Event-ID/Content-Type(只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)

不符合简单请求的条件,会被浏览器判定为特殊请求,例如请求方式为PUT;

(1)简单请求

当浏览器发现ajax请求是简单请求时,会在请求头中携带一个字段:Origin;Origin中会指出当前请求属于哪个域(协议+域名+端口),服务器会根据这个值决定是否允许其跨域;注意,这一步是浏览器自动完成的,在Header中自动添加Origin是HTTP协议,而实际中,还存在一个字段Referer,表示请求链接的来源,二者有类似的作用但也存在一些区别,具体可参考下面这篇文章《HTTP中Origin和Referer的区别》,在服务端做获取链接的来源操作时,二者建议都尝试获取,可参考文末的代码示例;

那么服务端要怎么处理才能让浏览器知晓当前请求允许跨域呢?简单来说,就是返回Response时,在Header中给几个特殊参数赋值

Access-Control-Allow-Origin: http://xxxx.com
Access-Control-Allow-Credentials: true
  • Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*,*代表任意;
  • Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true;

注意,如果跨域请求要想操作cookie,需要满足3个条件:

  • 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true;
  • 浏览器发起ajax需要指定withCredentials 为true;
  • 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名;

(2)特殊请求

特殊请求,对于浏览器的操作,多了一个步骤:特殊请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight);浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错;

对于服务端需要做的操作,没有太大区别,除了Access-Control-Allow-Origin和Access-Control-Allow-Credentials以外,Header中额外多出3个参数:

  • Access-Control-Allow-Methods:允许访问的方式;
  • Access-Control-Allow-Headers:允许携带的头;
  • Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了;

如果浏览器得到上述响应,则认定为可以跨域,后续就跟简单请求的处理是一样的了;

CORS原理的总结:

(1)浏览器会将ajax请求分为两类,其处理方案略有差异:简单请求、特殊请求,不同类型请求下,浏览器的操作和服务端告知浏览器允许跨域的操作略有不同;

(2)浏览器通过Header中的Origin参数和Referer参数,告知服务端请求链接的源站点信息;服务端取Header中的该信息来判断是否允许当前请求跨域;服务端通过向Header中的几个"Access-Control-Allow"前缀的参数赋值,来告知浏览器当前请求是否允许跨域;

其他的知识可参考这边文章《Web漏洞之CORS跨域资源共享漏洞 - 知乎》,里面讲述了关于CORS的更为详细的知识,包括:

1. CORS漏洞案例演示:操作过程、前端代码;
2. 为什么将Access-Control-Allow-Origin设置为*即允许任意源,为何还是会产生跨域问题?
3. CORS漏洞挖掘思考:查看响应头、判断请求源、漏洞危害及漏洞修复;

6. CORS漏洞示例

漏洞产生的原因一般是:服务端将Access-Control-Allow-Origin设置的值,匹配了恶意网站,如设置了通配符*;

7. 如何修复CORS漏洞——CorsFilter

根据上述原理,从Header中获取请求的源站点信息,判断是否白名单,若不满足则不在reponse的Header中填充对应的标识允许跨域的参数;反之,满足条件,则在response的Header中填充几个前缀为"Access-Control-Allow"的参数标识,告知浏览器允许跨域;

import com.AA.ConfigManager;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author AA
 * @description CorsFilter
 * @date 2021/3/15 14:58
 */
@Component
@WebFilter(filterName = "CorsFilter", urlPatterns = "/*")
public class CorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        // 打印请求信息
        // printRequestInfo(request);
        // 获取请求源站
        String origin = request.getHeader("origin");
        String referer = request.getHeader("referer");
        // 如果源站域名通过域名检查,则返回浏览器该域名可以进行跨域访问(referer返回*)
        // 如果不通过域名检查,不返回allow-origin,走默认逻辑,即不可进行跨域访问
        if (origin != null && check(origin)) {
            response.setHeader("Access-Control-Allow-Origin", origin);
        } else if (referer != null && check(referer)) {
            response.setHeader("Access-Control-Allow-Origin", referer);
        } else {
			log.error("CORS_NOT_PASS. [origin={} allowedOrigins={} headers={}]", originHeader, JSON.toJSONString(allowedOrigins),request.getRemoteAddr());
            printRequestInfo(request);
            ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
            serverResponse.setStatusCode(HttpStatus.FORBIDDEN);
            serverResponse.getBody().write("Invalid CORS request".getBytes(UTF8_CHARSET));
            serverResponse.flush();
			// 终止Filter 返回 403 forbidden
            return;
		}
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        chain.doFilter(req, res);
    }

    /**
     * CORS检查方法
     *
     * @param origin 要检查的源站地址
     * @return 检查结果
     */
    private boolean check(String origin) {
        // 获取跨域请求检查规则
        Pattern allowPattern = Pattern.compile(ConfigManager.getString("allow.origin", "^(https|http)?://([\\da-zA-Z.-]+).myDomian.com.cn"));
        Matcher matcher = allowPattern.matcher(origin);
        return matcher.find();
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }

    private void printRequestInfo(HttpServletRequest request) {
        try {
            StringBuilder sb = new StringBuilder();
            sb.append("\n").append("---remoteAddr:").append(request.getRemoteAddr());
            sb.append("\n").append("---Method:").append(request.getMethod());
            sb.append("\n").append("---url:").append(request.getRequestURL());

            sb.append("\n").append("---headers:");
            final Enumeration<String> headerNames = request.getHeaderNames();
            while (headerNames.hasMoreElements()){
                final String currentHeader = headerNames.nextElement();
                sb.append("\n").append(" ").append(currentHeader).append(":").append(request.getHeader(currentHeader));
            }
            sb.append("\n").append("---request_params:");
            Stream<String> stm = request.getReader().lines();
            stm.forEach(stm1 -> sb.append("\n").append(stm1));
            log.warn("printRequestInfo. {}", sb.toString());
        } catch (IOException ignored) {
        }
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值