# 1. XSS

Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。

为了和 CSS 区分,这里把攻击的第一个字母改成了 X,于是叫做 XSS。

XSS 的本质是:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。

而由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,或者利用这些信息冒充用户向网站发起攻击者定义的请求。

在部分情况下,由于输入的限制,注入的恶意脚本比较短。但可以通过引入外部的脚本,并由浏览器执行,来完成比较复杂的攻击策略。

这里有一个问题:用户是通过哪种方法“注入”恶意脚本的呢?

不仅仅是业务上的“用户的 UGC 内容”可以进行注入,包括 URL 上的参数等都可以是攻击的来源。在处理输入时,以下内容都不可信:

  • 来自用户的 UGC 信息
  • 来自第三方的链接
  • URL 参数
  • POST 参数
  • Referer (可能来自不可信的来源)
  • Cookie (可能来自其他子域注入)

# 1.1. XSS 分类

根据攻击的来源,XSS 攻击可分为存储型、反射型和 DOM 型三种。

类型 存储区 插入点
存储型 XSS 后端数据库 HTML
反射型 XSS URL HTML
DOM 型 XSS 后端数据库/前端存储/URL 前端 JavaScript
  • 存储区:恶意代码存放的位置。
  • 插入点:由谁取得恶意代码,并插入到网页上。

# 1.1.1. 存储型 XSS

存储型 XSS 的攻击步骤:

  • 攻击者将恶意代码提交到目标网站的数据库中。
  • 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
  • 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  • 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。

# 1.1.2. 反射型 XSS

反射型 XSS 的攻击步骤:

  • 攻击者构造出特殊的 URL,其中包含恶意代码。
  • 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
  • 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  • 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。

反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。

由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。

POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。

# 1.1.3. DOM 型 XSS

DOM 型 XSS 的攻击步骤:

  • 攻击者构造出特殊的 URL,其中包含恶意代码。
  • 用户打开带有恶意代码的 URL。
  • 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
  • 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。

# 1.2. 防范XSS

下面说法:

  1. XSS 防范是后端 RD 的责任,后端 RD 应该在所有用户提交数据的接口,对敏感字符进行转义,才能进行下一步操作。

不正确。因为:

防范存储型和反射型 XSS 是后端 RD 的责任。而 DOM 型 XSS 攻击不发生在后端,是前端 RD 的责任。防范 XSS 是需要后端 RD 和前端 RD 共同参与的系统工程。 转义应该在输出 HTML 时进行,而不是在提交用户输入时。

  1. 所有要插入到页面上的数据,都要通过一个敏感字符过滤函数的转义,过滤掉通用的敏感字符后,就可以插入到页面中。

不正确。 不同的上下文,如 HTML 属性、HTML 文字内容、HTML 注释、跳转链接、内联 JavaScript 字符串、内联 CSS 样式表等,所需要的转义规则不一致。 业务 RD 需要选取合适的转义库,并针对不同的上下文调用不同的转义规则。

# 2. CSRF

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

一个典型的CSRF攻击有着如下的流程:

  • 受害者登录a.com,并保留了登录凭证(Cookie)。
  • 攻击者引诱受害者访问了b.com。
  • b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会…
  • a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
  • a.com以受害者的名义执行了act=xx。
  • 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。

# 2.1. 几种常见的攻击类型

# 2.1.1. GET类型的CSRF

GET类型的CSRF利用非常简单,只需要一个HTTP请求,一般会这样利用:

<img src="http://bank.example/withdraw?amount=10000&for=hacker" > 

在受害者访问含有这个img的页面后,浏览器会自动向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker发出一次HTTP请求。bank.example就会收到包含受害者登录信息的一次跨域请求。

# 2.1.2. POST类型的CSRF

这种类型的CSRF利用起来通常使用的是一个自动提交的表单,如:

 <form action="http://bank.example/withdraw" method=POST>
    <input type="hidden" name="account" value="xiaoming" />
    <input type="hidden" name="amount" value="10000" />
    <input type="hidden" name="for" value="hacker" />
</form>
<script> document.forms[0].submit(); </script> 

访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作。

POST类型的攻击通常比GET要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许POST上面。

# 2.1.3. 链接类型的CSRF

链接类型的CSRF并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:

  <a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
  重磅消息!!
  <a/>

由于之前用户登录了信任的网站A,并且保存登录状态,只要用户主动访问上面的这个PHP页面,则表示攻击成功。

# 2.2. CSRF的特点

  • 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生。
  • 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据。
  • 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”。
  • 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。

CSRF通常是跨域的,因为外域通常更容易被攻击者掌控。但是如果本域下有容易被利用的功能,比如可以发图和链接的论坛和评论区,攻击可以直接在本域下进行,而且这种攻击更加危险。

# 2.3. 防护策略

CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性。

上文中讲了CSRF的两个特点:

  • CSRF(通常)发生在第三方域名。
  • CSRF攻击者不能获取到Cookie等信息,只是使用。

针对这两点,我们可以专门制定防护策略,如下:

  • 阻止不明外域的访问

    • 同源检测(Referer/Origin)
    • Samesite Cookie
  • 提交时要求附加本域才能获取的信息

    • CSRF Token,为什么Token可以防御CSRF呢,因为攻击者无法伪造Token
    • 双重Cookie验证

CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。

要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token。

防范 CSRF 可以遵循以下几种规则:

  • Get 请求不对数据进行修改
  • 不让第三方网站访问到用户 Cookie
  • 阻止第三方网站请求接口
  • 请求时附带验证信息,比如验证码或者 token

# 2.4. CSRF的攻击过程

根据上面介绍,登录态Cookie的Key是浏览器默认自动携带的,Key通常是会话Cookie,只要浏览器不关闭,Key一直存在。所以只要用户A曾经登录过相册网站(这里用www.photo.com举例),浏览器没有关闭,用户在没有关闭的浏览器打开一个黑客网页(这里用www.hacker.com),黑客页面发送HTTP请求到www.photo.com的后台会默认带上www.photo.com的登录态Cookie,也就能模拟用户A做一些增删改等敏感操作。Get和Post都一样,这就是CSRF攻击原理。这种攻击过程也是最常见的攻击过程,后面还会介绍另外一种少见的攻击过程。

# 2.5. 读操作能否被攻击到?

上面说的增删改都是写操作,会对后台数据产生负面影响,所以是能被攻击的。另外一种读操作,是具有幂等性,不会对后台数据产生负面影响,能否被攻击到?读操作也可能是敏感数据,举个例子,比如www.photo.com上的私密相册数据能否被www.hacker.com页面拿到?这就涉及到前端跨域知识点了,默认大部分情况是拿不到,这里列举两种特殊情是可以拿到的:

  • 如果后台返回的数据是JSONP格式的,这种只能是Get操作,是能被黑客页面拿到的。
  • 如果后台是通过CORS处理跨域,没有对请求头的Origin做白名单限制,ACAO响应头是*或者包括黑客页面,包括Get/Post/Del等操作,也是能被黑客页面拿到的。

除了这两种特殊情况,读操作都是不能被攻击到的,因为浏览器跨域限制是天然的安全的。关于跨域知识细节可以查看我另外一篇文章,跨域知识

# 2.6. Token

上面讲到Cookie的一些特性的第二条,读写Cookie有跨域限制(作用域,Domain,Path),所以我们可以用这个特性来区分是自己页面还是黑客页面。只要页面能读(或者写)www.photo.com域名Cookie,就证明是自己的页面。懂了原理,方案就很简单,比如服务器通过cookie下发一个token,token值是随机数,页面发请求的时候从cookie取出token通过HTTP请求参数传给后台,后台比对参数里的token和cookie里的token是否一致,如果一致就证明是自己页面发的请求,如果不一致就返回失败。防CSRF的方案就是这么简单,这种方法能够100%防CSRF,但是可能会有几个变种,下面探讨几种情况。

# 2.6.1. Token是前台生产还是后台生产?

我上面举例例子是后台生成传到前台的,大家发现其实后台并没有存这个token,所以原理上前后台生成都可以,只要保证随机性。如果前端生成token然后写到Cookie里,然后HTTP请求参数也带上token,后端逻辑一样比对参数里的token和cookie里的token是否一致,如果一致就证明是自己页面发的请求,如果不一致就返回失败。这就是Cookie读和写的差别,只要能读写自己域名的Cookie就是自己页面。

# 2.6.2. 公司内的方案

由于登录态已经下发了一个登录态skey,防CSRF的token就复用这个skey,由于登录态skey比较重要,尽量少明文暴露,所以前端拿到skey后做了一次Hash放到http请求参数里,后端通过同样的Hash算法对Cookie里的skey做Hash后跟参数里的token比对是否一致,如果一致就证明是自己页面发的请求,如果不一致就返回失败。公司内用的Hash算法是DJB算法(俗称“Time33”算法)。

# 2.6.3. Token放在HTTP参数里的哪里?

放在URL的querystring里,Post请求的Data里或者HTTP请求头里,这三种方式都可以,只是有一点点细微的差别,如果querystring里,可能会影响Get请求的缓存效果,因为重新登录之后token会变,url也就变了,之前的缓存就失效了。如果放在HTTP请求头里,就需要使用fetch或者XHR发请求,这样会变成复杂请求,跨域情况需要多一次Option预检请求,对性能多少有一点点影响。

# 2.6.4. 用Token方案后写操作可以用Get么?

可以,从安全角度考虑是可以的,用了Token之后,Get和Post的安全等级是一样的。上面讨论的那种少见的CSRF攻击过程也攻击不到了。但是从语义化考虑建议Get是还是处理读操作方便理解。

# 2.6.5. 用Token方案后读操作可以用JSONP跨域么?

可以,可以使用JSONP跨域了,另外如果使用CORS处理跨域,建议还是需要对请求头的Origin做白名单限制,防止不同子域名相互影响。

# 2.6.6. 如果页面有XSS漏洞,黑客拿到Cookie怎么办?

这个方法防不了XSS,防XSS需要其他方法,比如CSP,用户输入/输出做转义等。

# 3. 代码

修复路径穿越

if (filePath.indexOf('..') < 0) {
  try {
    const data = fs.readFileSync(filePath, 'utf8');
    return { err: null, data };
  } catch (err) {
    return { err };
  }
}

过滤sql注入

function filterData(data) {
  let fData = data;
  if (typeof data === 'string') {
    fData = data.replace('`', '');
  }
  return fData;
}

# 4. 相关资料

  1. 前端安全系列(一):如何防止XSS攻击? (opens new window)
  2. 前端安全系列之二:如何防止CSRF攻击? (opens new window)
  3. 这一次,彻底理解XSS攻击 (opens new window)
  4. xss-game (opens new window)
  5. prompt(1) to win (opens new window)
  6. alert(1) to win (opens new window)
  7. 什么是CSRF (opens new window)
  8. 细说CSRF (opens new window)