从 Web 应用程序的架构角度,可以将web应用大致分成两种:b/s架构和c/s架构。所以从web应用的组成实体上看,所谓的web安全其实就是考虑如何保障b(浏览器)、c(客户端)、s(服务端)这三方相关的安全。其实一般来说,经常将b归纳到c中,但是我们这里选择把他们分开。因为我们接下来会抛开c和s,仅仅只是聊一聊b,也就是浏览器相关的安全问题。
用户凭证的由来
浏览器安全还要从用户凭证安全讲起,而浏览器安全考虑的核心也是:如何保证用户凭证的安全。用户凭证的出现的促成因素主要有以下两点:
-
第一点是http的设计是一种无状态协议,也就是说请求之间没有维护关联关系,每个请求都是独立的,如果这种情况不加以处理,那么用户的每一步特殊操作都需要进行身份验证,这显然是十分麻烦的。
-
第二点是用户对于保持登陆状态的诉求。比如一个网站如果我天天访问的话,我肯定是希望登录一次就好,而不是每次访问都登陆一下。
为了解决上面这两个问题,设计了一种叫做用户凭证(token)的技术。这种技术其实就是在用户验证完身份之后,给用户颁发了一个通过一定算法生成的具有标识性质的令牌,后续的请求中只要携带这个令牌,服务器就见字如面为用户提供相应的特殊服务。
用户凭证的存储(浏览器本地存储)
浏览器为用户提供了 cookie
、session storage
、local storage
、indexDB
等等这些本地数据 存储工具。用户可以随便选择一个工具来存储凭证。而且更重要的是:如果粗略的讲:同源策略限制了跨域脚本不能读取本域的本地数据,这也为浏览器安全提供了最基本的安全保障。
其实对于本地数据读取的限制策略更准确地讲是同站策略 ,也就是说 www.example.com 源的脚本和 www.blog.example.com 源的脚本操作的是同一份本地数据。
同站(same-site)与同源(same-origin)的区别
- 同源要求协议、域名、端口三者需要全部完全相同。
- 同站指的是在网络上具有相同根域名(包括顶级域名)的网站或应用程序。例如:www.example.com 和 www.blog.example.com 是同一个站点,因为它们共享相同的根域名 example.com。同站和同源类似,不同的是同站对于域名的要求相对宽松一些。
关于特殊的 Cookie 存储介质
其实浏览器安全问题主要关注的也是 cookie。cookie具有一个特点:请求自动携带。而 session 登录技术就是结合 cookie 实现的,所以 cookie 被大量用在用户身份凭证保存的场景。
慢慢地我们对 cookie 的跨域携带限制要求越来越严格,从需要服务端放权(CORS:设置响应允许的源)到现在的除了需要服务端放权,还必须使用 https 才能跨域携带 cookie。
关于同源策略
同源策略防御有一个假设前提:同源脚本安全可靠,异源脚本不一定安全可靠。接下来我们可以分析两种场景下同源策略的作用:
普通网站中跨域请求恶意网站
一般情况下,普通网站是不会主动去访问恶意网站的。如果想要达到普通网站主动访问恶意网站,需要对用户进行流量劫持,进而实现修改网页内容。
这种情况下,同源策略几乎是没有作用的,因为通过修改网页内容插入的脚本是同源脚本,自然就可以轻而易举的读取该源(普通网站)的本地数据。
但是有朋友可能会想:同源策略不是限制了不能发跨域请求吗?那么插入的恶意脚本即使读到了本地数据,但是也无法将本地数据发送出去,这样不也是安全的。这个逻辑咋一看好像有点道理,其实仔细分析一些是不正确的。因为同源策略并不是限制请求能否发送,而是限制请求的响应是否处理,所以即使跨域,请求还是会发出去的。只有一种比较特殊的情况,就是对于复杂请求,如果预检请求没有处理跨域,那么后面的真实请求将不会发送。但其实这个问题也十分好解决,就是我的恶意服务器给他配一下 CORS 不就行了。
如果用户凭证存在 cookie 中,这种情况下可能是安全的。因为 cookie 可以通过设置 HttpOnly 来禁止脚本读取 cookie(即使是同源脚本)。而 cookie 的存储和携带 都是以源 作为操作单位(key),这里我们本意是想把 cookie 发送到恶意服务器,而由于发送的目标是恶意服务器,请求中自然不会携带普通网站的 cookie,所以对于设置了 HttpOnly 的 cookie,在这种场景下是安全的。
恶意网站中跨域请求普通网站
对于在恶意网站中访问普通网站,这种场景实现起来十分容易,只要搭建一个恶意web站点让用户访问即可。
由于同源策略的限制,恶意网站的脚本访问不了普通网站的本地数据,所以在这种场景下对于本地数据这一块来说是安全的。
关于 cookie 的携带,如果普通网站不给恶意网站源进行授权,那么在跨域情况下 cookie 默认是不会携带的,所以也是安全的。那如果普通网站给恶意网站的源进行了授权,被攻击了也是莫得办法的。
为什么浏览器的同源策略需要抛弃服务端不授权的Ajax响应结果?
回答这个问题需要回到:恶意网站中跨域请求普通网站这个场景中。
首先关于用户存储在服务器端的隐私数据的访问需要携带用户凭证。这种场景下本地数据肯定是取不到的,那么突破口只能是 cookie 自动携带。由于此时是跨域请求,所以 cookie 携带需要服务端(也就是普通网站服务器)授权后,浏览器才会真正发送 cookie。假设普通网站为了用户使用的方便性,将 cookie 携带权限授权给了任意源。
那么此时跨域请求就会携带上普通网站的 cookie,普通网站在验证 cookie 没问题之后响应用户的隐私数据。此时浏览器的同源策略做了最后一道防线,就是抛弃这个响应结果(如果普通网站没有进行授权的话)。
其实这里面包含一个逻辑悖论 ,那就是 cookie 跨域携带的授权和跨域结果获取的授权存在交叉 ,也就是 Access-Control-Allow-Origin
响应头的设置。也就是说:需要普通网站服务器设置 Access-Control-Allow-Origin
响应头为恶意网站源(可以通过动态设置的方案实现),这次的跨域请求 cookie 才会携带给到普通服务器。那么这么设置一般也就意味着跨域结果获取的授权也就完成了,除非请求方法(Access-Control-Allow-Methods)或者请求头(Access-Control-Allow-Headers)没有匹配上。
所以总的来说抛弃响应结果可以说是一种下下策了,没太大的作用。
关于JSOP
JSOP其实是一种比较特殊的技术手段,它利用了浏览器script标签可以加载任意脚本资源 和所有脚本加载完成后,最终都放到同一个全局环境中进行执行 的这两个特点。采用异源脚本编写方法实现,同源脚本编写方法使用。这样就可以将实现权让渡给第三方,自己掌握执行权,丰富网站的内容。由于同源脚本掌握着执行权,即使异源脚本提供恶意方法,只要我们的同源脚本不去执行,攻击也不会生效。(如果普通网站使用JSOP技术的话,也可以使用取巧的方法进行攻击,那就是构造恶意的同名方法)
由于最终是同源脚本去执行,所以如果访问了浏览器本地数据,实际上操作的是同源的本地数据。
关于CORS
想要理解CORS的意义首先要跳出一个误区,我们经常说 http 一请求一响应就结束。但是这其实是非常宏观的角度,从细节上看底层的 tcp 可是发生了多次数据交换的(也就是说可能先发一部分请求头内容,然后再发一部分响应头内容,然后再发响应头,回复响应体这样),使用过原生 xhr API 发请求的朋友应该有些感触。
cors 的意义也在于这里,就是如果我们需要跨域携带 cookie,那么是需要服务器告诉浏览器允许当前源的请求携带我方服务器源的 cookie 过来,这个时候浏览器才会真正地通过 tcp 发送 cookie 过去。也就是说 cors 实际上起到了一个给跨域 cookie 携带授权的作用。另外还有一个作用就是:给响应结果获取授权的作用。