web安全
同源策略
为了确保安全, 浏览器设计了同源策略, 限制一个源的文档(html)或者它加载的脚本(js)与另一个源进行交互.
源: 如果两个URL的协议, 端口(如果有端口的话)和主机都相同的话, 那么这两个URL是同源的.
对于文件源, 用 file:// 加载的文件通常被视为不透明的来源. 也就是说, 如果文件包含了同目录下的其它文件/目录, 这些文件不是同源的.
如index.html引用了file:///path/to/secret, 那么就会发生CORS错误, 因为不同源.
但是对于一些浏览器, 可能是同源的.
跨源操作
MDN把跨源操作分成三类:
- 跨源写: 一般是被允许的. 例如: 链接跳转和表单提交, 重定向. (特定的请求需要进行预检)
- 跨源读: 一般不被允许.
- 跨源资源嵌入: 一般被允许.
跨源写指进行一些可能修改另一个源的服务器状态的操作. 但是一般只能发送, 无法读取具体的数据(跨源读).
跨源资源嵌入一般指使用标签之类的进行嵌入, 比如
<script src="xxx"></script>
<img src="xxx">
<iframe>对于iframe嵌入, 目标源可以设置X-Frame-Options响应头来告诉浏览器不要让外部用iframe嵌入.
CORS
有的时候我们确实需要进行跨源读, 特别是现在前后端分离了.
浏览器提供了CORS(跨域资源共享)机制, 允许服务器设置响应头
- Access-Control-Allow-Origin: 允许源
- Access-Control-Allow-Headers: 允许头
- Access-Control-Allow-Methods: 允许的方法
- Access-Control-Expose-Headers: 预期的响应头
- Access-Control-Max-Age: 缓存的有效时长
如果需要使用cookie, 那么上面的都不允许为*
同时, 浏览器会在请求头自动加上Origin: 当前源
当试图跨源读且Origin与允许源不匹配时, 那么就会发生CORS错误.
预检请求
浏览器将请求分为两种: 简单请求和非简单请求.
简单请求应该是满足下列所有条件的请求:
- 方法是GET, POST, HEAD之一
- 只允许设置这些请求头
- Accept
- Accept-Language
- Content-Language
- Content-Type
- Range
- Content-Type只允许是这些值
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
所有不是简单请求的请求就是非简单请求.
对于简单请求, 浏览器不会做什么, 让它发送, 但是依旧有跨源读的限制. (允许跨源写)
对于非简单请求, 浏览器会先发送一个预检请求, 通常是这样的
OPTIONS /xxx HTTP/1.1
...
Origin: https://example.com
Access-Control-Request-Method: POST (将要使用的方法)
Access-Control-Request-Headers: Content-Type (将要使用的自定义请求头)如果服务器觉得可以, 那么就会返回200, 并带上CORS相关的响应头. 预检通过后, 浏览器才会发起真正的请求.
TLS
服务器与客户端之间通常有很多个节点, 为了简化, 大概分成三类:
- 服务端
- 客户端
- 中间人
服务端的信息经过中间人, 然后交给客户端; 客户端的信息也需要经过中间人, 然后交给服务端.
TCP+HTTP没有加密, 是明文传递. 因此中间人可以随意篡改传递的信息, 并且服务端和客户端都很难判断是不是对方发的.
TCP是可靠的传输协议, 但不是安全的传输协议.
因此就需要设计一个加密协议.
- 对称加密
如果使用对称加密, 那么客户端和服务端都需要一个相同的密钥. 在正式交流之前客户端必须把密钥发过去, 但是密钥的发送同样需要中间人, 这样密钥泄露的概率很大.
- 非对称加密
如果使用非对称加密, 客户端和服务端各持有一份公私钥, 在正式交流之前, 客户端先用私钥加密公钥, 发给服务端; 服务端用私钥加密这个加密后的公钥, 发给客户端; 客户端再用公钥解密发过去; 服务端再进行解密, 就拿到了公钥. 在这个过程中, 信息都是加密的, 因此公钥是安全的. 服务端的公钥传递也可以用相同的方法传递. 这样客户端和服务端都可以安全地进行通信了.
但是非对称加密比对称加密消耗的资源大, 服务端几乎不可能支撑这么大消耗(除非客户真的少).
btw, 这个其实也不是安全的, 中间人在握手阶段可以伪造成服务端欺骗客户端, 并伪造成客户端欺骗服务端. 原因是客户端不知道跟自己交流的到底是谁. 之后的证书其实就是解决这个问题.
因此, 这个协议必须将对称加密与非对称加密结合起来. 对称加密的密钥通过非对称加密传递.
- 客户端生成一个随机数, 然后传递给服务端.
- 服务端也生成一个随机数, 并和自己的公钥一起发给客户端
- 客户端再生成一个随机数, 并用服务端公钥加密发给客户端
- 服务端用私钥解密. 这样客户端和服务端都有三个相同的随机数, 然后计算出相同的密钥. 之后的交流就使用这个密钥进行对称加解密.
但是存在一个问题:
- 客户端生成一个随机数, 中间人也生成一个随机数, 然后发送自己的随机数.
- 服务端生成一个随机数和公钥发给中间人, 中间人也生成一个随机数, 再把自己的公钥和随机数发给客户端
- 客户端用中间人的公钥加密新的随机数, 发给中间人
- 中间人用自己的私钥解密, 这样中间人跟客户端计算出相同的密钥; 然后生成一个随机数用服务端的公钥加密发过去, 这样中间人和服务端也有相同的密钥. 之后客户端发的信息中间人可以破解, 服务端发的信息中间人也可以破解.
原因是客户端根本无法判断对方是服务端还是中间人, 只能当作服务端处理.
因此需要一个公证人.
证书
每个服务端可以去找一个公证人申请一份证书. 服务端需要将公钥和自己的一些信息, 如域名等, 交给公证人, 公证人再颁发证书.
为了确保证书无法被伪造, 公证人自己准备了一份公私钥匙.
拿到服务端的信息, 公证人先进行哈希, 然后使用私钥进行加密. 这个加密后的结果就是数字签名.
将服务端信息跟数字签名放到一起, 就是证书了.
公证人会自己给自己颁发一个证书, 然后让客户端去下载. 之后客户端使用证书上的公钥, 对服务端证书的数字签名进行解密, 然后匹配服务端证书信息的哈希值就可以判断对方是不是服务端了.
由于中间人无法伪造这个证书(除非公证人的私钥泄露了), 因此这样是安全的.
随着服务端增多, 一个公证人忙不过来, 因此需要更多的公证人. 但是公证人的增多, 导致公证人证书也变多了. 客户端很难来一个公证人就下载一个证书. 因此需要证书链.
根公证人自己的证书叫根证书. 它可以委托其它公证人, 给它们颁发中间证书. 然后这些中间公证人再给服务端颁发端实体证书.
客户端检查证书时, 只需要找给它们颁发端实体证书的中间证书, 然后往上找根证书. 如果找到了, 那么对方就是可信的.
为了确保安装根证书时没有中间人伪造, 一般根证书是系统预装的, 之后客户端也可以自己安装别的根证书.
显而易见的, 如果根证书是中间人伪造的, 那么中间人依旧可以随意伪造服务端.
完整的TLS握手流程
- Client Hello: 客户端生成一个随机数, 并发给服务端
这里客户端还有一个SNI字段, 表示客户端要访问哪个域名, 让服务端准备好.
- Server Hello: 服务端生成一个随机数, 并和自己的证书一起发给客户端
- Client Change Cipher Spec: 客户端生成一个随机数加密发过去并表示自己准备好了加密通话. 一般还会将握手信息用公钥加密发过去, 让服务端确认密钥是否正确.
- 服务端也会表示自己准备好了, 并将握手信息加密发过去.
XSS(Cross-Site scripting)
CSRF(Cross-Site request forgery)
攻击者欺骗用户点击恶意链接, 然后在恶意网站中向目标网站发送HTTP请求. 请求通常包含cookie, 导致服务器执行某些有害操作.
比如有段时间比较火的B站片哥片姐. 评论区全是麦片的. 要么是工作, 要么是在评论区点到了钓鱼链接.
由于一般人都会登录B站, 浏览器会存储相关的cookie.
当恶意网站向目标网站发送请求时, 由于cookie的特性, 请求会自动带上cookie. 如果请求恰好是简单请求(如表单填写等), 这个请求就会成功发出, 服务端也会以为这是用户干的, 进行处理.
防御方案
- 使用CSRF token
对于每一个表单, 都要求带上CSRF token. 这个token是服务端随机生成的, 进入页面或者别的什么时机发给客户端.
恶意网站本身是难以仿造这个token的, 因此也很难仿造表单提交.
- Sec-Fetch-Site请求头
这玩意是浏览器自动设置的, 告诉服务端是同源, 同站, 还是跨站, 或者none
- cross-site: 跨站(域名不同)
- same-origin: 同源(协议, 域名, 端口号相同)
- same-site: 域名相同
- none: 用户自己干的(比如地址栏输入URL)