简介

我们在维护web应用时候,都希望应用能够稳定地按照预期对用户提供服务,而不希望由于收到恶意攻击导致数据丢失、更改、泄露、服务中断。因此需要关注web安全。

安全三要素机密性、完整性、可用性,我们在评估我们web应用的安全性时候,可以从这几个方面入手。

安全三要素是安全的基本组成元素,分别是机密性(Confidentiality)、完整性(Integrity)、可用性(Availability)。

机密性要求保护数据内容不能泄露,加密是实现机密性要求的常见手段。

完整性则要求保护数据内容是完整、没有被篡改的。常见的保证一致性的技术手段是数字签名。

可用性要求保护资源是“随需而得”。

—— 《白帽子讲Web安全》 吴翰请

如果网站机密性不好,容易被攻击者获取用户的账号密码,会对用户的利益造成损失。

如果完整性不好,攻击者可能篡改信息,例如修改指定账号的存款金额,会对业务造成损失。

如果可用性不好,攻击者可能使用拒绝服务攻击,让服务崩溃,对业务造成严重损失。

攻击者对网站进行攻击可能有各种目的:获取机密信息;恶意攻击网站,导致不可用;恶意操作转账等等。

现针对网站有几个常见的攻击手段:XSS、CSRF、SQL注入、DDOS,这几个攻击方法的技术难度并不是很大(相对于渗透等),但是因为其更易于操作且对于攻击者回报更大,因此在实际情况中,占所有攻击方式的比例很大。

XSS

XSS攻击原理

跨站脚本攻击,英文全称是Cross Site Script,本来缩写是CSS,但是为了和层叠样式表(Cascading Style Sheet, CSS)有所区别,所以在安全领域叫做“XSS”。

XSS攻击,通常指黑客通过“HTML注入”篡改了网页,插入了恶意的脚本,从而在用户浏览网页时控制用户浏览器的一种攻击。在一开始,这种攻击的演示案例是跨域的,所以叫做“跨站脚本”。但是发展到今天,由于JavaScript的强大功能以及网站前端应用的复杂化,是否跨域已经不再重要。但是由于历史原因,XSS这个名字却一直保留下来。

恶意脚本能够注入页面的前提都是网站会把用户输入的数据通过各种方式渲染到页面上。

根据恶意脚本注入到页面中的方式,可以分为反射、存储、DOM-based。

反射型XSS攻击

如果网站是服务端渲染,并且会将URL的参数直接插入到页面HTML中,那就有可能存在反射型XSS漏洞。

攻击者可以构造URL并将参数替换为恶意脚本诱导用户点击(比如给用户发个免费领奖品的邮件),那么恶意脚本就会在用户打开的网页上执行。

这种方式将恶意脚本从URL“反射”到用户的浏览器上,因此这种攻击方式被称为“反射型”。

例如有这样一段PHP脚本,会将URL中的param参数展示到界面中

  1. <? php
  2. $input = $_GET["param"];
  3. echo "<div>".$input."</div>";
  4. ?>

如果攻击者构造这样一个URL

  1. http://www.a.com/test.php? param=<script>alert(/xss/)</script>

用户打开这个URL后将会加载一个script脚本并执行代码。

存储型XSS攻击

如果网站会将用户输入保存到数据库中并渲染到界面上(社区类型的网站常见的功能,注册用户都可以发表文章或者评论),那网站可能存在存储型XSS漏洞。

如果存在漏洞,攻击者可以作为注册用户发表文章或者评论,其他用户打开文章或评论将会执行恶意脚本。

由于恶意脚本会存储在服务端,当被攻击者浏览器页面时候从服务端取出在页面中执行,因此这种攻击方式被称为“存储型XSS攻击”。

DOM-based型XSS攻击

如果网站中会根据URL参数操作DOM内容,那么网站可能存在DOM-based的XSS漏洞。

攻击者可以构造URL并将参数替换为恶意脚本诱导用户点击(比如给用户发个免费领奖品的邮件),那么恶意脚本就会在用户打开的网页上执行。

DOM-based攻击也是反射型XSS攻击的一种,由于其特殊性被单独划分出来。由于这种攻击是通过DOM操作执行恶意脚本,因此被称为“DOM based型攻击”。

例如有这样一个页面

  1. <html>
  2. <head>
  3. <title>xss-test</title>
  4. </head>
  5. <body>
  6. <div id="root"></div>
  7. <a id="a" >link</a>
  8. <script>
  9. const href = new URL(location.href).search.slice(1).match(/url=([^&]+)/)[1];
  10. document.getElementById('a').href = decodeURIComponent(href);
  11. </script>
  12. </body>
  13. </html>

将连接中的”url”参数取出来设置到a标签的href属性中。现在我们构造这样一个链接

  1. localhost:8080?url=javascript:alert(%27dom-xss%27)

页面的a标签变成了

  1. <a id="a" href="javascript:alert('dom-xss')">link</a>

点击a标签将会执行”alert(‘dom-xss’)”代码。

XSS防御

恶意脚本能够注入页面的前提都是网站会把用户输入的数据通过各种方式渲染到页面上。因此防御的主要思想就是不要相信用户的输入。因此要对用户的输入进行编码。通常后端和前端都需要考虑对用户输入做处理。

根据处理的时机,XSS防御可以分为输入点校验和输出点校验。输入点就是在用户输入数据时候就对其编码;输出点指将数据渲染到DOM中时候对其校验和转换。

输入点防御有很大局限性,因为用户输入时候,并不知道输入的数据要用在哪里,没有办法针对性地处理。

React框架是在输出点进行防御,React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。

输出点过滤

浏览器解析HTML代码和JavaScript时候,输入都是字符串,在解析字符串时候,解析器需要区分代码和内容。

例如

  1. <div>test</div>

其中div标签就是HTML代码,而test是HTML中的内容,是要被展示在界面上的。

  1. console.log('test');

console.log()就是JavaScript代码,’test’字符串是内容,是要被处理的数据。

有时候我们在HTML中要展示的信息或者在JavaScript中要处理的数据和代码语法近似,容易被当做代码处理,例如:

  1. <div><span>test</span></div>

我们想在界面上展示“test”,但是如果我们像展示“test”文本那样,直接把要展示的内容写到div标签之中,就会被HTML解析器认为是一个span标签中有一个test文本。

再比如

  1. console.log('');('');

我们想打印“’);(‘”,但如果直接把这个字符串写到console.log方法中,将会被解析器认为是先打印一个空字符串,然后再执行一个没有意义的表达式语句“(‘’)”。

当然一般稍有经验的程序员就不会犯上面的错误。但是需要注意的是,当你的项目中有将用户输入作为HTML界面展示内容/元素属性,或者JavaScript处理内容时候,就可能被攻击者利用,构造恶意的输入数据,让本来作为展示的内容,被浏览器解析称为代码的一部分,从而执行恶意代码,达到攻击者的目的。

因此我们需要在将用户输入的数据放到HTML代码(DOM)/JavaScript代码中时候,要让HTML解析器和JavaScript解析器能够区分出来哪些是HTML代码(DOM)/JavaScript代码,哪些是HTML界面展示内容、元素属性/JavaScript处理内容。

我们可以通过编码来做到这一点。编码后的数据将被解析器认为不是代码的一部分。

HTML:字符实体编码

JavaScript:unicode编码

HTML字符实体编码语法:

  1. &entity_name;
  2. &#entity_number;

其中entity_number对应unicode码点。(关于unicode编码可以参考这篇文章:关于编码

例如,我们要展示“test”可以这样写

  1. <div>&lt;span&gt;test&lt;/span&gt;</div>

JavaScript编码语法

(1)\HHH

反斜杠后面紧跟三个八进制数(000到377),代表一个字符。HHH对应该字符的Unicode码点,比如\251表示版权符号。显然,这种方法只能输出256种字符。

(2)\xHH

\x后面紧跟两个十六进制数(00到FF),代表一个字符。HH对应该字符的Unicode码点,比如\xA9表示版权符号。这种方法也只能输出256种字符。

(3)\uXXXX

\u后面紧跟四个十六进制数(0000到FFFF),代表一个字符。HHHH对应该字符的Unicode码点,比如\u00A9表示版权符号。

例如我们希望打印“’);(‘”,可以这样写

  1. console.log('\x27\x29\x3b\x28\x27')

下面讨论在各种输出点场景下的防御。

不管那种场景,记住只要是用户输入的数据被用作HTML的展示内容/HTML的属性,就对数据进行HTML编码;只要是用户输入数据被用作JavaScript处理的内容,就对数据进行JavaScript编码)。

在HTML中输出

  1. <div>$var</div>

在这种场景下,XSS利用方式是构造一个script标签,或者任何能够产生JavaScript执行的方式。比如

  1. <div><script>alert('xss')</script></div>

或者

  1. <div><img src=# onerror="alert('xss')" /></div>

防御方法是对变量做HTML编码。

在属性中输出

  1. <div id="root" name="$var"></div>

攻击者可以通过先闭合引号然后构造script标签或者能够执行JavaScript的情况。

  1. <div id="root" name=""><script>alert('xss')</script></div>

或者

  1. <div id="root" name="" onclick="alert('xss')"></div>

防御方法也是对HTML进行编码。

在事件中输出

  1. <button onclick="func('$var')">click</button>

攻击者同样可以先闭合标签,再执行恶意代码

  1. <button onclick="func('');alert('xss');('')"></button>

防御方法是对变量进行JavaScript编码。

在script标签中输出

  1. <script>var x = "$var";</script>

攻击者可以先闭合引号,然后执行恶意代码。

  1. <script>var x = "";alert("xss");"";</script>

防御方式是对变量进行JavaScript编码。

在CSS中输出

一些老版本的浏览器会允许CSS中执行JavaScript,可能会带来XSS漏洞。

  1. div {
  2. background-image: url($var);
  3. }
  4. span {
  5. width: expression($expression);
  6. }

攻击者直接构造JavaScript代码就可以实现攻击:

  1. div {
  2. background-image: url(javascript:alert('xss'));
  3. }
  4. span {
  5. width: expression(alert('xss'));
  6. }

防御方式是对变量进行JavaScript编码。

cookie httpOnly

从cookie角度做防范并不能防止XSS恶意脚本注入,但是很多的XSS攻击的恶意脚本的直接目的是窃取用户的cookie,然后再通过cookie模拟用户身份,做一些有权限的操作,达到最终目的。

因此可以通过限制cookie操作来避免这类攻击。

服务端Set Cookie时 带上HttpOnly字段,阻止JavaScript获取Cookie。

CSP

内容安全策略CSP可以在一定程度避免网站收到XSS攻击影响。

通常恶意脚本注入后,不会马上开始攻击,因为通过漏洞注入的恶意代码不会很多,能做的事情有限,因此恶意脚本会通过动态创建标签加载外链脚本。如果能对外链做一些限制,就能在一定程度避免XSS攻击的影响。

如何使用CSP来阻止不安全的脚本注入到页面呢?答案是服务端在响应头中设置 Content-Security-Policy字段,指定该页面的脚本引入的策略,例如

  1. Content-Security-Policy: default-src 'self'

是指定只能加载同域的资源,例如script脚本、css样式表、图片、iframe

还可以指定某种类型的资源的策略

  1. Content-Security-Policy: default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com

这样指定图片可以加载任意源的资源,媒体只能加载 media1.com和media2.com域的资源,script只能加载userscripts.example.com域的资源。

设置响应头后,浏览器会block违反策略的行为。

CSP除了可以减少XSS,还能降低数据包嗅探攻击,即中间服务器可能解析流经的数据并窃听数据。CSP策略可以通过响应头的Strict-Transport-Security字段指定将http访问都重定向到https,这样浏览器会将访问该网站的请求都自动替换为https。这样避免了中间服务器的嗅探。

CSP除了可以在服务器响应头中指定,也可以在HTML标签中指定

  1. <meta http-equiv="Content-Security-Policy" content="default-src 'self';" />

其他

很多前端框架会对XSS攻击做处理,如React的JSX语法天然防御XSS,Vue中尽量避免使用v-html这种不安全的操作。

尽量不要使用eval执行JavaScript代码。

CSRF

CSRF攻击原理

CSRF(Cross-site request forgery),跨站请求伪造。指攻击者利用恶意代码向目标服务器发送请求,达到篡改数据、获取机密信息等目的。 下面简单描述一下CSRF攻击发生的过程。 正常用户日常访问一个网站www.biz.com(被攻击的网站),黑客自己建造一个网站www.evil.com,然后诱导用户访问,在www.evil.com中调用了www.biz.com网站的接口(比如获取用户信息、删除用户数据等恶意行为),由于www.evil.com网站在发送请求时候,是能够带上用户在www.biz.com域名下的cookie的,因此请求是合法的,也就能够成功完成攻击。 当然黑客也可以通过XSS攻击将恶意脚本注入一个用户常访问的网站www.other.com,然后在恶意脚本中发送请求攻击www.biz.com。这样即使www.biz.com里面的做了XSS的防御,也会被攻击。 CSRF攻击能够完成有几个关键点:
  1. 黑客能够让用户浏览器执行自己的恶意脚本(这一点很难防范,因为其他网站如果存在xss漏洞可能被黑客利用来攻击自己网站,没有办法保证所有网站都没有任何漏洞;另外如果黑客专门制造一个恶意网站并诱导用户点击就更难防范了)。
  2. 被攻击的网站允许跨域发送请求并允许跨域请求携带cookie。
  3. 被攻击的网站没有办法区分正常的请求和恶意的请求,因为cookie中有合法的用户信息,能够通过权限校验。

CSRF防御

上面CSRF防御发生的关键点中第一点很难避免,那么可以从第2、3点入手。

1. 禁止跨域访问

服务器可以设置响应头的”Access-Control-Allow-Origin“字段,禁止跨域访问,但是有些场景需要允许跨域访问。因此这种防御方法比较局限。

2. 通过origin、referer判断是否是合法域发送的请求

请求头中的origin是域名,referer中包含path信息。和第一种方法一样,这种防御方法也比较局限。而且由于origin和referer都可以伪造,因此这种方法并不能完全防御CSRF。

3. 禁止跨域携带cookie

服务端set cookie时候可以指定“SameSite”属性,这个属性有3个取值:

  1. strict,严格禁止跨域携带cookie
  2. lax,在get请求提交表单和a标签get请求时候可以携带cookie
  3. none,没有限制,允许跨域携带cookie

这个方法也有一定局限,因为有很多场景是需要跨域携带cookie的,例如你的页面使用了其他部分或者第三方提供的用户体系和后台服务,这时候前端的域名和后端的并无关系。

4. 使用csrf token区分是否是合法请求

csrf token用于让服务器判断请求是否合法,服务端渲染前端页面时候在页面中注入一个token(或者在首屏Ajax中下发一个token),下次发送请求时候前端将带上这个token,由于token是服务端颁发给前端的,因此服务端可以通过token判断请求是否合法。由于攻击者无法获取这个token,因此服务端可以区分合法的请求和预期之外的请求。

其他

常见网络攻击

web安全除了要关注最常见的XSS和CSRF攻击,还有一些其他有趣的网络攻击手段,其中有一些虽然攻击没有发生在web端,但是也和web有一定关系。

SQL注入

当后端进行数据库操作时候,如果SQL语句中包含了用户输入,就有可能导致SQL注入,其本质和XSS类似,攻击者构造输入,让拼接好的SQL语句在被解析时候,解析器没有区分开SQL语句和语句处理的参数。SQL注入可能导致拖库(指将网站的数据库被黑客下载到本地)或者DOS攻击。

DDOS

分布式拒绝服务攻击,黑客可能通过某些手段控制一些机器(肉鸡),在同一时间向指定服务器发送请求,导致服务器过载无法正常工作。在著名的中美黑客大战、圣战中,主要的攻击方法就是DDOS,虽然技术含量并不太高,但是造成的影响却比较大。

钓鱼

顾名思义,诱导用户点击某个网页,而这个网页是攻击者伪造的网站,当用户在其中输入了账号密码,或者操作转账等,就被黑客攻击了。

有很多钓鱼网站通过iframe加载了真实的网站,对其进行包装,用户打开的可能是一个伪造的“微博”,或者伪造的“银行转账”页面。为了避免网站被黑客包装用于钓鱼,可以禁止其他页面通过iframe加载。具体方法是设置相应头的相关字段。

X-Frame-Options:

X-Frame-Options可以设置三个值

  1. DENY 如果页面包含在框架中,则阻止呈现页面
  2. SAMEORIGIN 与上面相同,除非页面与顶级框架集所有者属于同一域
  3. ALLOW-FROM uri 页面只能被制定的uri嵌入到iframe 或 frame中

暴力破解

比较常见的黑客攻击行为就是盗号,思路比较简单,就是把常用的字符组合成一个字典,然后暴力尝试,盗取用户账号密码。服务器可以通过加验证码等避免账号密码被暴力破解。

当拖库发生后,如果用户账号密码都是明文存储的话,网站将面临巨大危险。所以一般网站只会存储用户的账号和密码的hash,但是这样也存在一定危险,就是黑客同样可以使用暴力破解法,根据常见密码<->hash的表查出密码。为了避免这种风险,可以考虑“加盐”hash,即对每个用户的密码随机生成一个字符串(salt),然后将字符串+用户密码合起来然后计算hash,由于每个账号都对应一个随机字符串,因此想要通过字典查到hash对应的密码,那字典的体积会非常大,根本不可能实现。

正则攻击

正则表达式在匹配一个字符串时候,可能会进行回溯操作,如果回溯次数过多会造成性能损耗,见下面代码

  1. console.time();
  2. /^(a+)+$/.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaab');
  3. console.timeEnd(); // default: 38181.561767578125 ms

当字符串中a增多时候耗时会迅速增大。

正则的这个原理可能会被用来ReDos攻击,让你的服务在执行一个耗时的正则时候卡主。

为了避免正则攻击,尽量不要执行用户输入的正则,项目中的正则也最好设置超时时长。

zip炸弹

有些文件压缩后很小,但是解压后非常大。比如著名的42.zip,只有42KB,解压后却有4.5PB。一旦被你的服务器或者客户端下载到本地,然后解压,然后,boom。

防御zip炸弹攻击最好在下载好文件后解压之前先通过一些方法计算解压后的大小。

DNS劫持

DNS劫持又称域名劫持,是指通过某些手段取得某域名的解析控制权,修改此域名的解析结果,导致对该域名的访问由原IP地址转入到修改后的指定IP,其结果就是对特定的网址不能访问或访问的是假网址。

DNS有很多危害

  1. 钓鱼
  2. 广告
  3. 流量都导入到恶意网站

用户可以通过手动修改DNS避免DNS劫持的风险。

网站可以使用HTTP DNS服务避免网站域名被DNS劫持,主流云计算厂商都提供这种服务。

P3P

P3P是一个人隐私保护策略平台(the Platform for Privacy Preferences),旨在保护用户在线隐私,让Internet用户在浏览网页时候可以设置是否允许第三方网站收集个人信息。

第三方网站是指通过第一方网站加载的网站,例如用户访问a.b.com,而a.b.com又通过iframe加载了x.y.com,那么a.b.com是第一方网站,x.y.com是第三方网站。

P3P协议要求服务器指定隐私策略(通过策略文件或者响应头),当用户访问第三方网站时候,浏览器解析网站的隐私策略,然后对比用户的设置,如果网站的策略不符合用户策略,则会给用户提示。

用户在浏览器中可以设置P3P相关的策略,如chrome的设置->隐私设置和安全性->Cookie及其他网站数据项中,可以设置禁止第三方cookie。