前言
去年找工作的时候,面了好几家大厂,在面试过程中发现大厂会比较注重前端安全。而在乙方工作,我发现周围的人无论是客户还是工程师,都比较忽视这类,很多人连漏洞的原理以及修复方式都搞不清楚,包括自己也有一些不清楚的地方。想了一下,一方面可能是因为前端/客户端的漏洞从攻击模型来看,往往依赖用户交互,有些攻击场景和修复方式都是基于用户端的,相比于直接攻击服务端来说,不太直观;另一方面也可以归因于人的浮躁。于是准备写一些文章,站在应用安全的角度梳理前端相关的安全问题,这篇文章主要设置前端安全的一些基础知识。
浏览器同源策略
所谓同源即对于URL来说,同协议、同主机、同端口即是同源。站在安全的角度,同源策略的限制主要是指跨源网络访问和跨源数据存储访问。跨域数据访问好理解,A站点上面JavaScript脚本无法读取到B站点上的类似Cookie
、LocalStorage
的数据;跨源网络访问即利用JavaScript发起HTTP请求,请求的目标地址和当前脚本的URL是非同源的,这个过程根据具体情况会收到一些限制,具体由CORS机制决定。而无论站在开发还是安全的视角,要解决问题就是什么情况下可以进行跨源请求,并携带资源(Cookie
、LocalStorage
等)。
跨源网络访问
前端的一些安全配置往往是针对浏览器的,为了便于理解浏览器的工作效果,我编写了一个简单的demo,用于验证各种安全配置对浏览器的影响,方便理解:
首先是服务端A(http://127.0.0.1:9999/cross_source_request.html),用于向用户提供页面:
1 | <script> |
其次是服务端B(https://192.168.98.1:5000/),用于提供实际的接口服务:
1 | from flask import Flask, make_response, request |
可以观察到两个服务是不同源的,在服务端A的JavaScript代码与服务端B的接口交互时,会涉及到跨域相关的问题,并且注意到我将服务端B的http服务配置了ssl证书,这涉及到后续介绍的一些浏览器配置要求,先暂时不提,如何给flask配置ssl可以参考这篇文章。
简单请求
结合跨源资源共享(CORS)和浏览器的同源策略的内容,总结可以跨源的情况。首先是不受同源策略的限制的情况:
跨源写操作:比如链接、重定向以及表单提交(这也解释了为什么常见的BurpSuite生成的CSRF POC可以起作用)
跨源的资源嵌入:
简单请求(即满足以下所有要求的请求):
可以看到简单请求的定义非常的苛刻,实际上现代Web开发过程中会频繁的涉及到跨域问题。在我们的demo中,因为存在自定义的非标准头(Authorization
),所以这是一个非简单请求,受到浏览器限制,无法成功请求。实际上浏览器发起了一个OPTIONS
请求,但是没有发出我们期望的GET
请求
可以看到默认情况下,浏览器限制了对于跨源站点的请求。那么对于跨源的情况,如何发送请求呢?
需预检的请求
实际上就是依靠第一个OPTIONS
方法的预检请求,根据此时服务器返回的配置信息来指示浏览器是否发起跨源请求。这里经常会有一道经典的面试题,A站点上的JavaScript向B站点发送跨域请求,为了让跨域逻辑正常运行,应该在哪个站点上进行CORS配置,答案是在被请求的站点上进行配置,因为资源是在被请求的站点上。控制跨域请求的常见的响应头如下:
- Access-Control-Allow-Origin:允许跨域请求的源(*代表任何源都可以请求,也可以指定具体的源)
- Access-Control-Allow-Credentials:是否可以携带对应站点的凭据
- Access-Control-Allow-Methods:真正请求时允许使用的方法
- Access-Control-Allow-Headers:实际请求中允许携带的标头字段
- Access-Control-Allow-Credentials:是否允许携带被访问站点的Cookie
在上一个例子中,可以在浏览器中看到报错信息,由于服务器缺少Access-Control-Allow-Origin
的配置导致浏览器拒绝了跨源的请求
Access to XMLHttpRequest at ‘https://192.168.98.1:5000/hello‘ from origin ‘http://127.0.0.1:9999‘ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource
那么现在我们来修改代码,来让上一个例子中的JavaScript可以发起跨域请求,并读取到响应:
1 |
|
现在开始测试,我们先访问,端点(http://192.168.98.1:9999/auth
)获取Cookie,在有Cookie的状态访问站点(http://192.168.98.1:9999/cross_source_request.html
),成功发起了跨源请求并读取到了响应值。
携带Cookie的跨源访问
在实际的开发过程中除了携带自定义的Header头部,我们也会遇到需要携带Cookie得到情况,那么如何携带Cookie进行跨源访问呢,需要满足两个条件:
XMLHttpRequest
等请求的API中设置类似withCredentials
选项为true,意为让请求时自动携带Cookie;- 同时在预见请求阶段,需要被跨源的站点给出一些配置,告诉浏览器允许在携带Cookie的情况下进行跨源请求,具体的配置如下
那么此时我们来修改代码,让demo能够进行携带Cookie的跨源请求,给服务端A添加一行req.withCredentials = true
1 | <script> |
修改服务端B代码,设置Access-Control-Allow-Credentials
字段为true,并且指定Access-Control-Allow-Origin
为一个具体的源
1 |
|
我们查看下效果,可以看到成功携带了Cookie:
实际上影响Cookie自动携带的因素还有一个,那就是Cookie的属性,在一开始我并没有在demo中设置Cookie属性。而现代浏览器比如Chrome和FireFox,当不设置Cookie的SameSite属性时,默认使用Lax策略,也就是无法跨域提交Cookie,此时你会发现,无论你怎么设置JavaScript请求API以及CORS头部,均无法自动携带Cookie进行跨域请求;所以只能显式的在demo中给Cookie设置上samesite='None'
属性,而现代浏览器又要求当存在samesite='None'
时必须同时设置secure
属性,即通过https传输,这也是为什么demo要配置ssl的原因
Same Site的定义
这一部分是刷完PortSwigger的CSRF的LAB后补充的,主要是区别同站和同源两个概念。同源上面已经介绍过了,同站主要是针对Cookie的,在Cookie中一个SameSite属性,这个属性正是用于限制跨站提交Cookie的行为,所以有必要了解清楚。直接引用PortSwigger的图片:协议和根域名完全一直就是Same Site(同站)否则算Cross Site(跨站)
Request from | Request to | Same-site? | Same-origin? |
---|---|---|---|
https://example.com |
https://example.com |
Yes | Yes |
https://app.example.com |
https://intranet.example.com |
Yes | No: mismatched domain name |
https://example.com |
https://example.com:8080 |
Yes | No: mismatched port |
https://example.com |
https://example.co.uk |
No: mismatched eTLD | No: mismatched domain name |
https://example.com |
http://example.com |
No: mismatched scheme | No: mismatched scheme |
Cookie安全属性
说到Cookie的定义,我想起毕业那会看过的面经,问Cookie和Session的区别,直到今天仍然觉得难蚌为啥要把这俩放一起说,可能是业务开发方向喜欢把这两者放在一起把。相比我比较喜欢BurpSuite插件开发中对Cookie的定义:一种参数。和URL参数、Body参数一样,Cookie也可以看成一种参数,因为他本身就是由一组key和value构成,他存放在浏览器端,而Session
、JSESSIONID
等,不过是用于鉴权的常用的Cookie参数名罢了。而由于Cookie存放在浏览器端,可以被JavaScript访问,所以就有被窃取的风险,而Cookie属性正是为了告诉浏览器如何处理涉及到Cookie的行为,具体涉及的属性如下:
Domain
指定了哪些主机可以接受 Cookie,并且Cookie的Domain是不区分端口号的,127.0.0.1:80和127.0.0.1:443被浏览器认为是相同的domain,均为127.0.0.1
Path
请求的URL必须包括这些路径
SameSite
在没有SameSite属性前,浏览器在发送每个请求时会自动携带对应站点的Cookie,这样会导致一些安全问题。SameSite属性用于告诉浏览器,在跨站点的请求中是否允许携带Cookie。可以选择三个值Strict
(不允许发送第三方Cookie)、Lax
(比Strict稍微宽松一点,当导航到第三方站点时可以发送Cookie)和 None
(不做限制但Cookie必须通过https传输,即设置Secure属性)
Secure属性
Cookie只能通过https发送
HttpOnly属性
设置后JavaScript的document.cookie
API无法获取Cookie内容。
至此,常见的一些前端安全基础配置梳理完毕,当然还有一些比如CSP之类的配置,因为和具体的漏洞,比如XSS相关,就放在另外的文章里。
参考
- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies
- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
- https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
- https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/withCredentials
- https://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html