<audio id="audio" title="06 | XSS：当你“被发送”了一条微博时，到底发生了什么？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c9/0f/c91815434c8e74d21fc9d6fd9908bd0f.mp3"></audio>

你好，我是何为舟。

在前面的课程中，我们重点讲解了安全的一些基础知识，更多地是从宏观的层面上来谈论安全。但**安全不是一个靠宏观指导就能够落地的东西**。因此，接下来我会结合真实案例中的各种安全问题，来介绍具体的安全防护手段和工具。今天，我们就先从最基础的Web安全开始。

在Web安全这个模块中，我们所谈论的Web，是指所有基于HTTP或者其他超文本传输协议（RPC等）开发的应用，包括：网页、App、API接口等等。这类应用的共同点是：通过HTTP等文本协议，在客户端和服务端之间进行数据交换。客户端需要将服务端传出的数据展示渲染出来，服务端需要将客户端传入的数据进行对应的处理。而Web安全所涉及的正是这些应用中存在的各类安全问题。

背景介绍完了，下面我们进入今天的正题。

基于前面安全基础知识的学习，你现在通过了面试官的考核，成功进入了这家公司。某一天，公司的网页应用中发生了一件事。

有很多用户发送了同样类型的内容，而且这些内容都是一个带有诱惑性的问题和一个可以点击的链接。这些用户全部反馈说，这不是他们自己发的。前端开发表示，用户内容都是后端产生的，他不负责。后端开发表示，这些内容都是用户自己提交上来的，他也不负责。正当大家议论纷纷的时候，你作为学习过安全专栏的人，敏锐地发现了问题的原因：这是黑客发起了XSS攻击。

这个事情的原型，其实是2011年微博真实出现的一次安全事件。整个事件的核心问题，其实出在这个可以点击的链接上。在这个事件中，黑客并不需要入侵到微博服务器中，只要用户点击了这个链接，就会“被发送”这样的博文。

这就是著名的XSS攻击所能够实现的效果。那么，XSS攻击究竟是怎么产生的呢？我们究竟该如何防护呢？今天我就带你来了解这个网页中最经典的XSS攻击。

## XSS攻击是如何产生的？

首先，我们来看，XSS攻击是如何产生的。作为最普遍的网页语言，HTML非常灵活，你可以在任意时候对HTML进行修改。但是，这种灵活性也给了黑客可趁之机：通过给定异常的输入，黑客可以在你的浏览器中，插入一段恶意的JavaScript脚本，从而窃取你的隐私信息或者仿冒你进行操作。这就是**XSS攻击**（Cross-Site Scripting，跨站脚本攻击）的原理。

你现在应该对XSS有了一个大致的了解，除此之外，你还需要了解三种XSS攻击，它们分别是：反射型XSS、基于DOM的XSS以及持久型XSS。下面我们一一来看。

### 1.反射型XSS

假设现在有一个搜索网页，当你输入任意一个关键词，并点击“搜索”按钮之后，这个网页就会给你展示“你搜索的结果内容是：XXX”。

<img src="https://static001.geekbang.org/resource/image/a8/63/a80e92776bad0f35e8d7030806165163.jpg" alt="">

我们以PHP为例，这个网页的服务端实现逻辑如下所示：

```
&lt;!DOCTYPE html&gt;
&lt;html&gt;
	&lt;body&gt;
		&lt;form role=&quot;search&quot; action=&quot;&quot; method=&quot;GET&quot;&gt;
			&lt;input type=&quot;text&quot; name=&quot;search&quot; placeholder=&quot;请输入要搜索的内容&quot;&gt;
	        &lt;button type=&quot;submit&quot;&gt;搜索&lt;/button&gt;
	    &lt;/form&gt;
		&lt;?php
			if (isset($_GET['search']) &amp;&amp; !empty($_GET['search'])) {
				$search = $_GET['search'];
				echo &quot;&lt;h3&gt;你搜索的结果内容是：&quot; . $search . &quot;&lt;/h3&gt;&quot;;
			}
		?&gt;
	&lt;/body&gt;
&lt;/html&gt;


```

我们可以看到，这段代码的逻辑是将搜索框输入的内容，拼接成字符串，然后填充到最终的HTML中。而且这个过程中没有任何的过滤措施，如果黑客想要对这个过程发起攻击，他会输入下面这行代码：

```
&lt;/h3&gt;&lt;script&gt;alert('xss');&lt;/script&gt;&lt;h3&gt;

```

黑客输入这段字符后，网页会弹出一个告警框（我自己测试的时候，发现部分浏览器，如Safari不会弹出告警框，这是因为浏览器自身提供了一定的XSS保护功能）。

<img src="https://static001.geekbang.org/resource/image/f0/69/f0d2d16b482c9bbc5388250f2d34cc69.jpg" alt="">

通过查看网页的源码，可以发现，这中间多了一段JavaScript的脚本：

<img src="https://static001.geekbang.org/resource/image/29/98/29cff8464c5dd7b88dafb377e0439b98.jpg" alt="">

这就是我们所说的反射型XSS攻击的过程。其实它攻击的原理很简单。我们可以总结一下，即通过开头的`&lt;/h3&gt;`和结尾的`&lt;h3&gt;`，将原本的`&lt;h3&gt;`标签进行闭合，然后中间通过`&lt;script&gt;`标签插入JavaScript代码并执行，就完成了整个反射型XSS的流程。

你可以注意一下浏览器的地址：[http://localhost/index.php?search=&lt;%2Fh3&gt;&lt;script&gt;alert('xss')%3B&lt;%2Fscript&gt;&lt;h3&gt;](http://localhost/index.php?search=%3C%2Fh3%3E%3Cscript%3Ealert%28%27xss%27%29%3B%3C%2Fscript%3E%3Ch3%3E) 。实际上，任何人只要点击了这个链接，就会执行一段黑客定义的JavaScript脚本。所以，我们经常说，不要点击任何未知的链接。

反射型XSS的总体流程我总结了一下，你可以看下面这张图。黑客诱导你点击了某个链接，这个链接提供的服务，可能就是上述的搜索功能。网页在解析到链接的参数后，执行正常的搜索逻辑，但是因为漏洞，网页中被填入了黑客定义的脚本。使得用户的浏览器，最终执行的是黑客的脚本。

<img src="https://static001.geekbang.org/resource/image/b8/2c/b85f24cbc8243426fb270bcb74be682c.jpeg" alt="">

### 2.基于DOM的XSS

在上面的例子中我们可以看到，反射型XSS产生在前后端一体的网页应用中，服务端逻辑会改变最终的网页代码。但是，目前更流行的其实是前后端分离，这样网页的代码不会受服务端影响。那么，这样是不是就安全了呢？

显然不是的。尽管服务端无法改变网页代码，但网页本身的JavaScript仍然可以改变。而黑客只要利用了这一点，同样能够在网页中插入自己的脚本。这也就是所谓的基于DOM的XSS漏洞。

对于上述搜索功能，通过前后端分离，它的源码就变成了下面这样：

```
&lt;!DOCTYPE html&gt;
&lt;html&gt;
	&lt;body&gt;
		&lt;form role=&quot;search&quot; action=&quot;&quot; method=&quot;GET&quot;&gt;
			&lt;input type=&quot;text&quot; name=&quot;search&quot; placeholder=&quot;请输入要搜索的内容&quot;&gt;
	        &lt;button type=&quot;submit&quot;&gt;搜索&lt;/button&gt;
	    &lt;/form&gt;
	    &lt;script&gt;
	    	var search = location.search.substring(8);
	    	document.write('你搜索的结果内容是：' + decodeURIComponent(search));
	    &lt;/script&gt;
	&lt;/body&gt;
&lt;/html&gt;

```

这段代码能够实现和之前的PHP代码相同的逻辑：当你在搜索框点击搜索关键词之后，网页会展示你输入的关键词。只不过，HTML是通过JavaScript脚本修改[DOM](https://baike.baidu.com/item/%E6%96%87%E6%A1%A3%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B/1033822?fromtitle=DOM&amp;fromid=50288&amp;fr=aladdin)来实现这个功能的。

那么和上述例子一样，在基于DOM的XSS中，黑客也可以通过插入一段`&lt;script&gt;alert('xss');&lt;/script&gt;`来执行指定的JavaScript脚本。基于DOM的XSS总体流程如下图所示。可以看到，这个流程其实和反射型XSS一致，只是不需要经过服务端了而已。

<img src="https://static001.geekbang.org/resource/image/ac/bf/ac0ce9c0f42db7dd48cb155228b7b1bf.jpeg" alt="">

### 3.持久型XSS

你可以回想一下，当你在网页中搜索一个关键词时，实际上与这个关键词相关的所有搜索结果都会被展示出来。一旦这些搜索结果中，包含黑客提供的某个恶意JavaScript脚本，那么只要我们浏览了这个网页，就有可能会执行这些脚本。这就是持久型XSS。因为这些恶意的搜索结果，会长期保存在服务端数据库中，所以它又叫作存储型XSS。在应用中，存储用户的输入并对它们进行展示的地方，都可能出现持久型XSS。比如：搜索结果、评论、博文等等。

有了前面的铺垫，持久型XSS的产生过程就很好理解了，具体我就不细说了，我还是把总体流程画了一张图，你可以仔细看看。

<img src="https://static001.geekbang.org/resource/image/2e/75/2e8c429f46837b3f46a276e462c93175.jpeg" alt="">

相比前面两种XSS攻击来说，持久型XSS往往具备更强的危害性。因为对于一个反射型或者基于DOM的XSS来说，需要黑客诱导用户点击恶意的URL，才能够成功地在用户浏览器上执行JavaScript脚本。这对黑客在诱导用户操作方面的能力提出了考验：并不是所有的用户都是小白，一些有经验的用户会在点击链接前进行一定的考虑。

而持久型XSS则不同，它是将恶意的JavaScript脚本写入到了正常的服务端数据库中，因此，只要用户正常的使用业务功能，就会被注入JavaScript脚本。所以说，持久型XSS在传播速度和传播范围上，会远远超出其他类型的XSS。

## 通过XSS攻击，黑客能做什么？

我们知道，这3种XSS攻击，都是因为黑客在用户的浏览器中执行了恶意的JavaScript脚本。那么执行这些JavaScript脚本有什么样的危害呢？我把这些危害总结了一下，可以分为下面几种。

### 1.窃取Cookie

从上面的例子中，我们可以看到，黑客可以窃取用户的Cookie。因为黑客注入的JavaScript代码是运行在server.com这个域名下的，因此，黑客可以在JavaScript中通过document.cookie获得Cookie信息。

另外，需要我们注意的是，受[SOP](https://baike.baidu.com/item/%E5%90%8C%E6%BA%90%E7%AD%96%E7%95%A5/3927875?fr=aladdin)（Same Origin Policy，同源策略）保护，我们在server.com中是无法直接向hacker.com发送GET或者POST请求的。这也是为什么，在上面的例子中，我们需要通过window.location来执行跳转操作，间接地将Cookie信息发送出去。除了window.location之外，我们还可以通过加载JavaScript文件、图片等方式，向attacker.com发送带有Cookie的GET请求。

### 2.未授权操作

除了窃取敏感信息以外，黑客还可以利用JavaScript的特性，直接代替用户在HTML进行各类操作。

在文章开头，我们提到的微博XSS攻击事件中，黑客就利用JavaScript脚本，让用户发送了一个微博，微博中同时还带有反射型XSS的链接。这样一来，每个点击链接的用户都会通过微博的形式，诱导更多的用户点击链接，一传十、十传百，造成大范围的传播。

### 3.按键记录和钓鱼

窃取Cookie和未授权操作都是我们很容易想到的危害，除此之外，JavaScript还能做什么呢？

JavaScript的功能十分强大，它还能够记录用户在浏览器中的大部分操作。比如：鼠标的轨迹、键盘输入的信息等。也就是说，你输入的账号名和密码，都可以被JavaScript记录下来，从而被黑客获取到。

另外，即使某个存在XSS漏洞的页面不具备任何输入框，黑客还可以通过修改DOM，伪造一个登录框，来诱导用户在本不需要登录的页面，去输入自己的用户名和密码。这也是“钓鱼”的一种形式，在这个过程中用户访问的域名是完全正常的，只是页面被篡改了，所以具备更高的迷惑性。

## 如何进行XSS防护？

认识到XSS的危害之后，作为开发人员，我们最应该掌握的是，如何避免在开发过程中出现XSS漏洞。接下来我们就来看一看，具体有哪些防护方法。

### 1.验证输入OR验证输出

防护的核心原则是：**一切用户输入皆不可信**。你的第一反应一定是，这很好实现啊，当接收到用户的输入时，我们就进行验证，这不就做到了吗？实际上并不是这么简单的，我们还是通过搜索这个例子来看。在用户点击“搜索”按钮之后，如果我们马上对他输入的内容进行验证，这样就会产生两个问题。

1.你将无法保存用户的原始输入信息。这样一来，当出现了Bug或者想要对黑客行为进行溯源时，你只能“推断”，而不能准确地获取用户的原始输入。

2.用户的内容可能会被多种语言获取和使用，提前编码或者处理，将产生未知的问题。比如，在旧版本的PHP中，就存在“[magic quotes](https://www.php.net/manual/en/security.magicquotes.php)”的漏洞，因为PHP无法处理某些编码的字符而导致崩溃。

**因此，我更推荐在需要输出的时候去进行验证**，即当需要展示的时候，我们再对内容进行验证，这样我们就能够根据不同的环境去采取不同的保护方案了。

在HTML中，常见的XSS注入点我已经总结好了，你可以看下面这个表格：

<img src="https://static001.geekbang.org/resource/image/7c/a0/7c1b9b3da33247963c2c1bf38ae184a0.jpeg" alt="">

### 2.编码

现在，我们已经理解了，**XSS防护的核心原则就是验证**，那具体该怎么去做验证呢？我认为，我们可以优先采用编码的方式来完成。所谓编码，就是将部分浏览器识别的关键词进行转换（比如&lt;和&gt;），从而避免浏览器产生误解。对于客户端来说，编码意味着，使用JavaScript提供的功能对用户内容进行处理。具体的方法我也总结了一下，你可以看这个表格。

<img src="https://static001.geekbang.org/resource/image/ca/1e/ca06351dcf763b86bb8bef554763bc1e.jpeg" alt="">

对于最后一个注入点，即在JavaScript中进行注入，目前还没有内置的编码方式来对它提供保护。你当然可以通过诸如URL编码等方式进行编码，但这有可能对应用的自身逻辑产生影响。因此，JavaScript中的注入并不适合通过编码来进行保护。

### 3.检测和过滤

但是，在很多时候，编码会对网页实际的展现效果产生影响。比如，原本用户可能想展示一个1&gt;0，却被编码展示成了1&amp;gt0。尽管网络环境安全了，却对用户造成了困扰。那么，我们还可以采取哪些方法进行验证呢？接下来我就为你介绍一下检测和过滤。

首先，我们需要对用户的内容进行检测。在这里，我们可以采用黑名单和白名单的规则。黑名单往往是我们最直接想到的方法：既然黑客要插入`&lt;javascript&gt;`标签，那么我们就检测用户内容中是否存在`&lt;javascript&gt;`标签就好了。

但是，黑客的攻击方法是无穷无尽的。你检测了`&lt;javascript&gt;`，黑客就可以改成`&lt;JavaScript&gt;`（因为HTML标签对大小写不敏感），甚至有些时候还能够编码成`&amp;#106;avascript`等等。另外，HTML5的发展速度很快，总是有新的标签被开发出来，这些新标签中也可能包含新的注入点。因此，黑名单的更新和维护过程，是需要我们和黑客进行长期对抗的过程

所以，在检测中，**我更推荐使用白名单的规则**。因为白名单的规则比较简单，并且十分有效。比如，在只输入一个分数的地方，规定只有整型变量是合法的。这样一来，你就能够检测出99.99%的攻击行为了。

说完了检测，那当发现某个用户的内容可能存在XSS攻击脚本时，我们该怎么处理呢？这个时候，处理选项有两个：拒绝或者过滤。毫无疑问，拒绝是最安全的选项。一旦你发现可能的XSS攻击脚本，只要不将这段用户内容展现出来，就能避免可能的攻击行为。

但是，拒绝会阻碍用户的使用流程，从用户体验的角度上来考虑的话，过滤会更被用户所接受。上面提到的编码就属于一种过滤的方式。除此之外，我们也可以直接对敏感字符进行替换删除等。需要注意的是，在替换的时候，一定不能采取黑名单的形式（比如：将javascript进行删除，那黑客就可以通过JavaScript来绕过），而是**应该采取白名单的形式**（比如，除了div之外的标签全部删除）。

同样地，**过滤的流程也必须彻底**。比如，我看到过有人采用下面这行字符串来过滤javascript标签：

```
$str=str_replace('&lt;javascript&gt;','',$str);

```

但黑客只需要将str的值变成`&lt;java&lt;javascript&gt;script&gt;`就可以了，因为`str_replace('&lt;javascript&gt;','','&lt;java&lt;javascript&gt;script&gt;')`的结果就是`&lt;javascript&gt;`。

### 4.CSP

面对XSS这样一个很普遍的问题，W3C提出了CSP（Content Security Policy，内容安全策略）来提升Web的安全性。所谓CSP，就是在服务端返回的HTTP header里面添加一个Content-Security-Policy选项，然后定义资源的白名单域名。浏览器就会识别这个字段，并限制对非白名单资源的访问。

配置样例如下所示：

```
Content-Security-Policy:default-src ‘none’; script-src ‘self’; 
connect-src ‘self’; img-src ‘self’; style-src ‘self’;

```

那我们为什么要限制外域资源的访问呢？这是因为XSS通常会受到长度的限制，导致黑客无法提交一段完整的JavaScript代码。为了解决这个问题，黑客会采取引用一个外域JavaScript资源的方式来进行注入。除此之外，限制了外域资源的访问，也就限制了黑客通过资源请求的方式，绕过SOP发送GET请求。目前，CSP还是受到了大部分浏览器支持的，只要用户使用的是最新的浏览器，基本都能够得到很好的保护。

## 总结

好了，我们讲了XSS的攻击类型、会产生的影响，以及如何对它进行防护。下面我来带你总结回顾一下，你要掌握的重点内容。

简单来说，XSS就是利用Web漏洞，在用户的浏览器中执行黑客定义的JavaScript脚本，这样一种攻击方式。根据攻击方式的不同，可以分为：反射型XSS、基于DOM的XSS和持久型XSS。通过在用户的浏览器中注入脚本，黑客可以通过各种方式，采集到用户的敏感信息，包括：Cookie、按键记录、密码等。

预防XSS主要通过对用户内容的验证来完成。首先，我推荐在需要展示用户内容的时候去进行验证，而不是当用户输入的时候就去验证。在验证过程中，我们优先采用编码的方式来完成。如果编码影响到了业务的正常功能，我们就可以采用白名单的检测和过滤方式来进行验证。除此之外，我们可以根据业务需要，配置合适的CSP规则，这也能在很大程度上降低XSS产生的影响。

另外，在这里，我把本节课的重点内容梳理了一个脑图。你可以根据它来查漏补缺，加深印象。

<img src="https://static001.geekbang.org/resource/image/48/20/48a923998b80ad2f1c2a274704690e20.jpg" alt="">

## 思考题

好了，通过今天的学习，相信你已经了解了什么是XSS攻击。你可以试着分析一下，文章开头提到的事件中，黑客是利用哪种类型的XSS发起的攻击呢？我们应该怎么进行防御呢？

另外，在事件中我也描述了开发“甩锅”的场景：前端也好、后端也好，开发人员都不认为是自己的问题。那么，你认为出现这种安全事件，应该由谁来“背锅”呢？是开发、运维还是安全负责人呢？

欢迎留言和我分享你的思考和疑惑，也欢迎你把文章分享给你的朋友。我们下一讲再见！
