Fork me on GitHub

m-通信安全

有待补充
参考 前端 | XSS 的攻击手段及其防御
参考 前端 | CSRF 的攻击类型与防御
参考 前端安全系列(二):如何防止 CSRF 攻击?
参考 前端安全系列(一):如何防止 XSS 攻击?

XSS cross-site scripting

跨域脚本攻击

代码注入攻击,攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可以获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。

本质:恶意代码未经过滤,与网站正常代码混在一起,浏览器无法辨别哪些监本可信,导致恶意脚本执行。由于直接在用户的终端执行,恶意代码能够直接获取用户信息,或者利用这些信息冒充用户向网站发起攻击者定义的请求。在部分情况下,由于输入的限制,注入的恶意脚本比较短。但可以通过引入外部的脚本,并由浏览器执行,来完成比较复杂的攻击策略。

用户是通过哪种方法“注入”恶意脚本的呢?

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

分类

类型 存储区 插入点
Reflected XSS URL HTML
Stored XSS 后端数据库 HTML
DOM XSS 后端数据库、前端存储、URL 前端 JS
  • 存储区:恶意代码存放的位置。
  • 插入点:由谁取得恶意代码,并插入到网页上。
  1. Stored XSS(存储型 )

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

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

  2. Reflected XSS(反射型)

    • 攻击者构造特殊的 URL,其中包含恶意代码
    • 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,并拼接在 HTML 上返回给浏览器
    • 用户浏览器接收到响应后解析执行,混在其中的恶意代码被执行
    • 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
      这是一种非持久型的攻击。常见于通过 URL 传递请求参数的功能,如网站搜索,跳转等,POST 请求内容也会触发反射型 XSS,只是触发条件比较苛刻(需要构造表单提交页面,并引导客户点击),所以少见。
      反射型与存储型的区别:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
  3. DOM XSS

    • 攻击者构造出特殊的 URL,其中包含恶意代码
    • 用户打开带有恶意代码的 URL
    • 页面加载时,前端 JavaScript 取出 URL 的恶意代码执行
    • 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
      DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。

      举例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      <input type="text" id="input">
      <button id="btn">Submit</button>
      <div id="div"></div>
      <script>
      const input = document.getElementById('input');
      const btn = document.getElementById('btn');
      const div = document.getElementById('div');

      let val;

      input.addEventListener('change', (e) => {
      val = e.target.value;
      }, false);

      btn.addEventListener('click', () => {
      div.innerHTML = `<a href=${val}>testLink</a>`
      }, false);
      </script>

      点击 Submit 按钮后,会在当前页面插入一个链接,其地址为用户的输入内容。如果用户在输入时构造了如下内容: onclick=alert(/xss/)用户提交之后,页面代码就变成了:<a href='onclick="alert(/xss/)"'>testLink</a>

防御方式

XSS 攻击有两大要素:1. 攻击者提交恶意代码 2. 浏览器执行恶意代码
输入过滤

  1. 在用户提交时,由前端过滤输入。
    不可行。攻击者绕过前端过滤,直接构造请求,也可以提交恶意代码。
  2. 后端对输入数据进行过滤、转义,再把数据写入数据库或返回前端
    因为不确定输入的内容会输出到哪里,前端不同位置所需编码不同,可能会照成显示异常。
    例如,5 < 7被服务器转义成5 &lt; 7
    在 HTML 中<div>5 &lt; 7</div>可以正常显示,
    5 &lt; 7 通过 Ajax 返回,然后赋值给 JavaScript 的变量时,前端得到的字符串就是转义后的字符。这个内容不能直接用于 Vue 等模板的展示,也不能直接用于内容长度计算。不能用于标题、alert 等。

所以,输入侧过滤能够在某些情况下解决特定的 XSS 问题,但会引入很大的不确定性和乱码问题。在防范 XSS 攻击时应避免此类方法。
对于明确的输入类型,例如数字、URL、电话号码、邮件地址等等内容,进行输入过滤还是必要的。
输出过滤

  • 防止 HTML 中出现注入
  • 防止 JavaScript 执行时,执行恶意代码。

预防存储型和反射型 XSS 攻击
存储型和反射型 XSS 都是在服务端取出恶意代码后,插入到响应 HTML 里的,攻击者刻意编写的“数据”被内嵌到“代码”中,被浏览器所执行。

预防这两种漏洞,有两种常见做法

  • 改成纯前端渲染,把代码和数据分隔开。
    React/Vue
    纯前端渲染的过程:

    • 浏览器先加载一个静态 HTML,此 HTML 中不包含任何跟业务相关的数据。
    • 然后浏览器执行 HTML 中的 JavaScript。
    • JavaScript 通过 Ajax 加载业务数据,调用 DOM API 更新到页面上。
      注意避免 DOM 型 XSS 漏洞
  • 对 HTML 做充分转义。
    如果拼接 HTML 是必要的,就需要采用合适的转义库,对 HTML 模板各处插入点进行充分的转义。
    常用的模板引擎,如 doT.jsejsFreeMarker 等,对于 HTML 转义通常只有一个规则,就是把 & < > " ' / 这几个字符转义掉,确实能起到一定的 XSS 防护作用,但并不完善:
    |XSS 安全漏洞|简单转义是否有防护作用|
    |-|-|
    |HTML 标签文字内容|有|
    |HTML 属性值|有|
    |CSS 内联样式|无|
    |内联 JavaScript|无|
    |内联 JSON|无|
    |跳转链接|无|

更完善更细致的转义策略: Java 工程里,常用的转义库为org.owasp.encoder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!-- HTML 标签内文字内容 -->
<div><%= Encode.forHtml(UNTRUSTED) %></div>

<!-- HTML 标签属性值 -->
<input value="<%= Encode.forHtml(UNTRUSTED) %>" />

<!-- CSS 属性值 -->
<div style="width:<= Encode.forCssString(UNTRUSTED) %>">

<!-- CSS URL -->
<div style="background:<= Encode.forCssUrl(UNTRUSTED) %>">

<!-- JavaScript 内联代码块 -->
<script>
var msg = "<%= Encode.forJavaScript(UNTRUSTED) %>";
alert(msg);
</script>

<!-- JavaScript 内联代码块内嵌 JSON -->
<script>
var __INITIAL_STATE__ = JSON.parse('<%= Encoder.forJavaScript(data.to_json) %>');
</script>

<!-- HTML 标签内联监听器 -->
<button
onclick="alert('<%= Encode.forJavaScript(UNTRUSTED) %>');">
click me
</button>

<!-- URL 参数 -->
<a href="/search?value=<%= Encode.forUriComponent(UNTRUSTED) %>&order=1#top">

<!-- URL 路径 -->
<a href="/page/<%= Encode.forUriComponent(UNTRUSTED) %>">

<!--
URL.
注意:要根据项目情况进行过滤,禁止掉 "javascript:" 链接、非法 scheme 等
-->
<a href='<%=
urlValidator.isValid(UNTRUSTED) ?
Encode.forHtml(UNTRUSTED) :
"/404"
%>'>
link
</a>

预防 DOM 型 XSS 攻击
前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行。
在使用.innerHTML.outerHTMLdocument.write()时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而尽量使用.textContent.setAttribute()等。

如果用Vue/React技术栈,并且不使用v-html/dangerouslySetInnerHTML 功能,就在前端 render 阶段避免innerHTMLouterHTML 的 XSS 隐患。

DOM 中的内联时间监听器,如locationonclickonerroronloadonmouseover等,a 标签的href属性,JS 的eval()setTimeout()setInterval()等,都能把字符串当做代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。

其他 XSS 防范措施

  1. CPS:W3C 的Content Security Policy

    • 禁止加载外域代码,防止复杂的攻击逻辑
    • 禁止外域提交,网站被攻击后,用户的数据不会被泄漏到外域
    • 禁止内联脚本执行(规则比较严格)
    • 禁止未授权的脚本执行
    • 合理使用上报可以及时发现 XSS,利于尽快回复

    使用方法:

    • 服务器添加 Content-Security-Policy 响应头来指定规则
    • HTML 中添加 <meta> 标签来指定 Content-Security-Policy 规则
  2. 输入内容长度控制

    对于不受信任的输入,都应该限制一个合理的长度,虽然无法完全防止 XSS 发生,但可以增加攻击难度。

  3. HTTP-only Cookie
    浏览器将禁止页面的 Javascript 访问带有 HttpOnly:true 属性的 Cookie。
    严格来说,HttpOnly 并非阻止 XSS 攻击,而是能阻止 XSS 攻击后的 Cookie 劫持攻击。

  4. 验证码:防止脚本冒充用户提交危险操作。

CSRF cross-site request forgery

跨站攻击伪造

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

流程

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

常见的攻击类型

  • GET 类型的 CSRF
    ![](https://awps-assets.meituan.net/mit-x/blog-images-bundle-2018b/ff0cdbee.example/withdraw?amount=10000&for=hacker)
    用户访问含有这个 img 的页面后,浏览器会自动向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker发出一次 HTTP 请求。bank.example就会收到包含受害者登录信息的一次跨域请求。
  • POST 类型的 CSRF
    通常使用的是一个自动提交的表单,如:

    1
    2
    3
    4
    5
    6
    <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 操作。

  • 链接类型的 CSRF
    需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:

1
2
3
 <a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
免费领iPhone
<a/>

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

特点

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

防护策略

CSRF 通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对 CSRF 的防护能力来提升安全性。
自动防御即利用 HTTP 协议固有的特性进行自动防护,而主动防御则需要通过编程手段进行防御。

自动防御

利用同源策略阻止不明外域的访问

直接禁止外域(或者不受信任的域名)对我们发起请求。
在 HTTP 协议中,每一个异步请求都会携带两个 Header,用于标记来源域名:

  • Origin Header
  • Referer Header

这两个 Header 在浏览器发起请求时,大多数情况会自动带上,并且不能由前端自定义内容。 服务器可以通过解析这两个 Header 中的域名,确定请求的来源域。

Origin Header
如果存在 Origin Header,直接利用其来确认来源域名。
Origin Header 不存在

  • IE11 同源策略,不会在跨站 CORS 请求上添加 Origin,只使用 Referer
  • 302 重定向之后不包含 Origin,因为 Origin 可能被认为是其他来源的敏感信息。

Referer Header

根据 HTTP 协议,在 HTTP 请求头中有个字段 Referer 记录该请求的来源地址,对于 Ajax 请求、图片和 script 等资源请求,Referer 是发起请求的页面地址,对于页面跳转,Referer 为打开页面历史记录的前一个页面地址,因此我们使用 Referer 中链接的 Origin 部分可以得知请求的来源域名。

但是 Referer 的值是浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不是很安全。在部分情况下,攻击者可以隐藏,甚至修改自己请求的 Referer。

Referrer Policy 草案
设置方法:

  1. 在 CSP 设置
  2. 页面头部增加 meta 标签
  3. a 标签增加 referrerpolicy 属性

例如:把 Referrer Policy 的策略设置成 same-origin,对于同源的链接和引用,会发送 Referer,referer 值为 Host 不带 Path;跨域访问则不携带 Referer。例如:aaa.com 引用 bbb.com 的资源,不会发送 Referer。

无法确认来源域名情况
当 Origin 和 Referer 头文件不存在时该怎么办?如果 Origin 和 Referer 都不存在,建议直接进行阻止,特别是如果您没有使用随机 CSRF Token(参考下方)作为第二次检查。

如何阻止外域请求
通过 Header 的验证,我们可以知道发起请求的来源域名,这些来源域名可能是网站本域,或者子域名,或者有授权的第三方域名,又或者来自不可信的未知域名,并阻止请求。
但是,当一个请求是页面请求(比如网站的主页),而来源是搜索引擎的链接(例如百度的搜索结果),也会被当成疑似 CSRF 攻击。所以在判断的时候需要过滤掉页面请求情况,通常 Header 符合以下情况:

1
2
Accept: text/html
Method: GET

但相应的,页面请求就暴露在了 CSRF 的攻击范围之中。如果你的网站中,在页面的 GET 请求中对当前用户做了什么操作的话,防范就失效了。

例如,下面的页面请求:

1
GET https://example.com/addComment?comment=XXX&dest=orderId

注:这种严格来说并不一定存在 CSRF 攻击的风险,但仍然有很多网站经常把主文档 GET 请求挂上参数来实现产品功能,但是这样做对于自身来说是存在安全风险的。
另外,前面说过,CSRF 大多数情况下来自第三方域名,但并不能排除本域发起。如果攻击者有权限在本域发布评论(含链接、图片等,统称 UGC),那么它可以直接在本域发起攻击,这种情况下同源策略无法达到防护的作用。

同源验证是一个相对简单的防范方法,能够防范绝大多数的 CSRF 攻击。但这并不是万无一失的,对于安全性要求较高,或者有较多用户输入内容的网站,我们就要对关键的接口做额外的防护措施

主动防御:提交时要求附加本域才能获取的信息

利用特征,攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用 Cookie 中的信息。

CSRF Token
要求所有的用户请求都携带一个 CSRF 攻击者无法获取到的 Token。服务器通过校验请求是否携带正确的 Token,来把正常的请求和攻击的请求区分开,也可以防范 CSRF 的攻击。

具体步骤:

  1. 服务器利用加密算法对数据(一般是随机字符串+时间戳)进行加密生成一个 Token,将 CSRF Token 输出到前端中
  2. 页面提交请求携带 Token,通常隐藏在表单域中作为参数提交,或拼接在 URL 后作 query 提交
  3. 服务器验证 Token 是否正确

服务器收到前端的请求需要判断 Token 的有效性,验证过程是先解密 Token,对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么这个 Token 就是有效的。这种 Token 的值通常是使用 UserID、时间戳和随机数,通过加密的方法生成。这样的加密既能验证请求的用户、请求的时间,又能保证 Token 不容易被破解。

这种方法要比之前检查 Referer 或者 Origin 要安全一些,Token 可以在产生并放于 Session 之中,然后在每次请求时把 Token 从 Session 中拿出,与请求中的 Token 进行比对。

双重 Cookie 验证
利用 CSRF 攻击不能获取到用户 Cookie 的特点,我们可以要求 Ajax 和表单请求携带一个 Cookie 中的值。
具体步骤:

  1. 用户访问网站页面时,向请求域注入一个 Cookie,内容为随机字符串。
  2. 在前端向后端发起请求时,取出 Cookie,并添加到 URL 的参数中
  3. 后端接口验证 Cookie 中的字段与 URL 参数中的字段是否一致,不一致则拒绝。
    将 token 设置在 Cookie 中,在提交 POST 请求的时候提交 Cookie,并通过 header 或者 body 带上 Cookie 中的 token,服务端进行对比校验。

比 CSRF Token 简单,但是存在缺陷没有大规模应用,因为跨域无法获取 Cookie 中的字段(包括子域名之间的)。由于任何跨域都会导致前端无法获取 Cookie 中的字段(包括子域名之间),所以当用户访问我的 me.ursb.me 之时,由于我的后端 api 部署在 api.ursb.me 上,那么在 me.ursb.me 用户拿不到 api.ursb.me 的 Cookie,也就无法完成双重 Cookie 验证。依此,我们的 Cookie 放在了 ursb.me 主域名下,以保证每个子域名都可以访问。但 ursb.me 下其实我还部署了很多其他的子应用,如果某个子域名 xxx.ursb.me 存在漏洞,虽然这个 xxx.ursb.me 可能没有什么值得窃取的信息,但是攻击者可以修改 ursb.me 下的 Cookie,从而实现 XSS 攻击,并利用篡改的 Cookie 对 me.ursb.me 发起 CSRF 攻击。同时,为了确保 Cookie 传输安全,采用这种防御方式的最好确保用整站 HTTPS 的方式,如果还没切 HTTPS 的使用这种方式会有风险。

用双重 Cookie 防御 CSRF 的优点:

  • 无需使用 Session,适用面更广,易于实施。
  • Token 储存于客户端中,不会给服务器带来压力。
  • 相对于 Token,实施成本更低,可以在前后端统一拦截校验,而不需要一个个接口和页面添加。

缺点:

  • Cookie 中增加了额外的字段。
  • 如果有其他漏洞(例如 XSS),攻击者可以注入 Cookie,那么该防御方式失效。
  • 难以做到子域名的隔离。
  • 为了确保 Cookie 传输安全,采用这种防御方式的最好确保用整站 HTTPS 的方式,如果还没切 HTTPS 的使用这种方式也会有风险。

Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie 是个“同站 Cookie”,同站 Cookie 只能作为第一方 Cookie,不能作为第三方 Cookie,Samesite 有两个属性值,分别是 Strict 和 Lax,下面分别讲解:

  • Samesite=Strict

这种称为严格模式,表明这个 Cookie 在任何情况下都不可能作为第三方 Cookie,绝无例外。比如说 b.com 设置了如下 Cookie:

1
2
3
Set-Cookie: foo=1; Samesite=Strict
Set-Cookie: bar=2; Samesite=Lax
Set-Cookie: baz=3

在 a.com 下发起对 b.com 的任意请求,foo 这个 Cookie 都不会被包含在 Cookie 请求头中,但 bar 会。

  • Samesite=Lax
    这种称为宽松模式,比 Strict 放宽了点限制:假如这个请求是这种请求(改变了当前页面或者打开了新页面)且同时是个 GET 请求,则这个 Cookie 可以作为第三方 Cookie。比如说上面设置的 b.com 的 Cookie。
    当用户从 a.com 点击链接进入 b.com 时,foo 这个 Cookie 不会被包含在 Cookie 请求头中,但 bar 和 baz 会,也就是说用户在不同网站之间通过链接跳转是不受影响了。但假如这个请求是从 a.com 发起的对 b.com 的异步请求,或者页面跳转是通过表单的 post 提交触发的,则 bar 也不会发送。
    缺点
    Samesite 的兼容性不是很好。
    SamesiteCookie 不支持子域

防止网站被利用

CSRF 的攻击可以来自:

  • 攻击者自己的网站。
  • 有文件上传漏洞的网站
  • 第三方论坛等用户内容
  • 被攻击网站自己的评论功能等

对于来自黑客自己的网站,我们无法防护。但对其他情况,那么如何防止自己的网站被利用成为攻击的源头:

  • 严格管理所有的上传接口,防止任何预期之外的上传内容(例如 HTML)
  • 添加 Header X-Content-Type-Options: nosniff 防止黑客上传 HTML 内容的资源(例如图片)被解析为网页。
  • 对于用户上传的图片,进行转存或者校验。不要直接使用用户填写的图片链接。
  • 当前用户打开其他用户填写的链接时,需告知风险(这也是很多论坛不允许直接在内容中发布外域链接的原因之一,不仅仅是为了用户留存,也有安全考虑)。
-------------本文结束感谢阅读-------------