CORS请求&CSRF攻击

发布于 2021-06-11  275 次阅读


在最近的前后端分离项目开发过程中,频频遇到了CORS请求的问题。趁着思路还清晰,在这里做个备忘笔记。

本文部分内容参考自以下来源,在此深表感谢:
MDN官方文档 - 跨源资源共享(CORS)
跨域资源共享 CORS 详解 - 阮一峰的网络日志

什么是CORS?

根据MDN给出的定义:

跨源资源共享 (CORS) (或通俗地译为跨域资源共享)是一种基于HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它origin(域,协议和端口),这样浏览器可以访问加载这些资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的"预检"请求。在预检中,浏览器发送的头中标示有HTTP方法和真实请求中会用到的头。

举例来讲,假设前端页面运行在http://localhost:8080上,当用户访问前端资源,例如http://localhost:8080/index.html, http://localhost:8080/1.html 等,对这些资源的请求都不会涉及跨域问题。而当浏览器需要访问运行在http://localhost:8081上的后端资源时,因为端口号并不相同,浏览器便会执行跨域请求。

MDN给出的CORS请求示意图

前段定义中涉及到了一个关键的概念:Origin,也就是我们常说的域,或者源。我们可以将所有的HTTP请求归类为同源请求和跨域请求。我们通过以下几条来判断一个请求是否跨域:

  • 协议类型(HTTP/HTTPS)
  • 域名
  • 端口号

只有当前页面和被请求资源的以上三条都一致的时候才是同源请求,否则一律按跨域请求处理。这里需要注意的是,域名匹配时不会进行DNS转换,也就是说http://localhost:8080和http://127.0.0.1:8080同样不在一个域内。

CORS请求

根据被发送请求的具体内容不同,我们可以将HTTP请求分为简单请求和非简单请求。在处理简单请求时,浏览器不需要发送预检(Preflight)请求,而在处理复杂请求时,浏览器会首先向服务端发送OPTION预检请求来获取服务器是否允许以CORS方式访问资源。有关预检,我们将在稍后具体介绍。

简单请求

当HTTP请求方式为以下之一:

  • GET
  • HEAD
  • POST

且HTTP请求首部仅包括以下字段:

且Content-Type字段取(application/x-www-form-urlencodedmultipart/form-datatext/plain)三者之一时,该请求才可被视作简单请求。

请求和响应报文

# 请求报文
GET /resources/public-data/ HTTP/1.1 
Host: bar.other 
Accept-Language: en-us,en;q=0.5
Connection: keep-alive 
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html 
Origin: http://foo.example 
# 响应报文
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: application/xml

请求报文中的Origin以及相应报文的Access-Control-Allow-Origin共同构成了最基本的首部字段。Origin字段表明请求的来源,服务器返回的Access-Control-Allow-Origin指定了服务器接受的跨域访问源。*则表示接受所有外部跨域访问。

预检请求

预检请求需要在实际请求发送前向服务端发送OPTIONS请求来获取服务端是否支持CORS,支持的Origin,支持什么请求以及支持什么header。

请求和响应首部

 # 请求首部
 
 # 实际请求方法
 Access-Control-Request-Method: POST 
 # 实际请求包括的headers
 Access-Control-Request-Headers: X-PINGOTHER, Content-Type 
 # 响应首部
 
 Access-Control-Allow-Origin: http://foo.example
 Access-Control-Allow-Methods: POST, GET, OPTIONS
 Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
 # 请求有效期:在有效期内,浏览器无需重复发送OPTIONS预检请求
 Access-Control-Max-Age: 86400 

XSS&CSRF攻击

XSS攻击

XSS(Cross-site-Script跨站脚本攻击),通常是带有页面可解析内容的数据未经处理直接插入到页面上解析造成的。XSS根据攻击脚本的引入位置来区分为存储型XSS、反射型XSS以及MXSS(也叫DOM XSS)三种。

存储型XSS

多见于博客、论坛等允许用户输入内容的页面。xss注入脚本会被持久化到数据库中。举最简单的例子,在后端程序没有对用户输入进行任何处理的情况下,用户输入:

<script>alert("This is low level")</script>

当浏览器接受后端传回的数据并解析到<script>标签时,便会加载这段代码实现攻击。

反射型XSS

多见于攻击者直接提供URL给用户,引导用户点击并进行攻击。例如:

https://malicious.link/q="%2F><script>alert%28document.cookie%29</script>

如果网页中的HTML是用到了query的参数,便会解析以上攻击代码,进而暴露用户的cookies。

常见的防御方法有:将诸如<>的特殊字符替换为HTMl转义符号,或通过正则表达式匹配括号内容并进行替换。

CSRF攻击:冒充用户之手

CSRF的全称是Cross Site Request Forgey。简单可以理解为攻击者盗用了用户的身份,以用户的名义发送恶意请求。

What is Cross-site Request Forgery (CSRF)? - Creative Ground Technologies
CSRF攻击示意

如上图所示,典型的CSRF攻击分为四步:

  • 用户登入某受信任网站A
  • 官方网站A生成Cookie并存储于用户端浏览器
  • 在不登出A的情况下,访问黑客提供的钓鱼网站B
  • 钓鱼网站利用用户的浏览器发出请求,利用<img src="xxx">等方式绕过同源检测机制

CSRF防御

(1)验证 HTTP Origin&Referer 字段

黑客在钓鱼网站向受信网站发送请求时,会将自身的域名放入Origin(IE11-, 302重定向除外)和Referer字段内,且无法被前端代码修改。当进行关键操作时,后端服务器需要验证HTTP请求头部是否来自受信任的地址。

(2)添加CSRF Token

根据上面的攻击流程,我们总结可见,黑客只能利用用户的浏览器发送cookie,并不能直接获得该cookie。因此,我们可以在后端生成csrf token,并存储于后端session中。将该token输入到页面中,并在前端页面表单内加入隐藏的csrf token字段,该字段值通常使用用户名、时间等进行hash加密。当服务器收到请求时,需要验证token的加密字符串是否正确和时间戳是否过期。