[TOC]

一、XSS

即 Cross Site Script (跨站脚本攻击),由于缩写与 CSS 冲突,故称 XSS

指的是攻击者在网络上注入恶意的客户端代码,通过恶意脚本堆客户端网页进行篡改,从而在用户浏览网页时,对用户浏览器进行控制 或者 获取用户隐私数据的一种攻击方式。

注入的恶意脚本一般包括:JS,有时也会包含HTMLFlash
XSS攻击可以分为以下 3

  • 反射型(非持久型)
  • 存储型(持久型)
  • 基于 DOM

她们的共同点为:将一些隐私数据如 cookiesession发送给攻击者,将受害者重定向到一个由攻击者控制的网站,在受害者的机器上进行一些恶意操作。

XSS 的类型

1. 反射型

用户通过 Web 客户端提交给服务端的数据,立刻用于解析和显示该用户的结果页面(数据没有在服务端存储)
如果提交的数据中含有恶意的脚本代码,而服务端没有经过编码转换或者过滤,就会形成 XSS 攻击,这种形式的 XSS 称为反射型 XSS

常见的通过浏览器地址栏输入的 HTTP GET请求参数 和 页面搜索框输入的 POST 查询内容 恶意用户通过构造含恶意脚本的 URL,发送到各种群、朋友圈、邮箱,诱导用户点击,获取点击用户的信息,达到攻击目的。

2. 存储型

用户通过Web客户端提交给服务端的数据,由服务端保存,然后永久显示在其他用户的页面上。
如果提交的数据中含有恶意的脚本代码,而服务端在存储前或展示前没有经过编码转换或者过滤,就会形成 XSS攻击,这种形式的 XSS 称为存储型 XSS

常见的场景是攻击者,在社区或论坛上,写下一篇包含恶意JS代码的文章或评论,文章或评价发表后,所有访问该文章或评论的用户,都会他们的浏览器中执行这段恶意的代码。

XSS 和 CSRF - 图1

3. 基于 DOM

指通过恶意脚本修改页面的 DOM结果,是纯粹发生在客户端的攻击。
如:

<h2>XSS: </h2>
<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>

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

XSS 和 CSRF - 图2

XSS 攻击的防御措施

现在主流的浏览器内置了防范 XSS 的措施,例如 CSP 。但还是要寻找其他可靠的解决方案来防止 XSS 攻击

1. HttpOnly 防止截取 Cookie

浏览器将禁止页面的 JS访问带有 HttpOnly属性的Cookie
严格来说,它并非XSS攻击,而是能阻止 **XSS**攻击后的 Cookie劫持

2. 输入检查

对于用户的任何输入要进行检查、过滤和转移。建立可信任的字符 和 HTML标签白名单,对于不在 白名单列内的字符 或者 标签,进行过滤或编码

一般是检查用户输入的数据中是否包含<>,等特殊字符,如果存在则对特殊字符进行处理,这种方式称作 XSS Filter

3. 输出检查

用户的输入会存在问题,服务端的输出也会存在问题。
一般来说,除富文本的输出外,在变量输出到 HTML页面时,可以使用编码或转移的方式来防御 XSS 攻击

4. CSP

二、CSRF

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

  • 跨站请求伪造(CSRF)
    • 跨站:B 域名网站(恶意网站) 访问 A 域名网站的接口
    • 请求伪造:B 网站通过某种方式获取 m 用户的 cookie,伪造为 m 用户,通过 cookie 的身份验证,访问 A 网站中 m 用户权限下的接口

通过某种方式是什么方式?

  • 方式一:黑客知道了 m 用户的账号密码,登录后,拿到 cookie
  • 方式二:
    • 在 m 用户登录了 A 网站之后,A 服务器把 set-Cookie,然后 A 网站就会有 cookie,在 cookie 没有过期或者销毁之前,m 用户被诱导打开了 B 网站
    • 这时 B 网站访问了 m 在 A 网站的接口,浏览器会自动携带上 m 的 cookie 发给 A 网站

浏览器为啥要带上 cookie 发给 A 呢,不是明明是在 B 网站访问的 A 服务器的接口吗,说好的浏览器的同源策略呢? 答:在 B 网站用 ajax 请求,浏览器当然不会把 m 用户在 A 网站的 cookie 发给 A 的服务器的,这符合同源策略。 但是呢,有允许跨域的标签啊,允许跨域的属性啊,利用这些,B 网站发送的请求,浏览器就会携带 m 用户的 cookie 发给 A 的服务器。

XSS 和 CSRF - 图3

CSRF 攻击示例

假如现在 A 服务器有三个接口,接口是用 cookie 来验证身份的。

更新用户信息(POST):http://localhost:3000/users/update 获取用户详细信息(GET):http://localhost:3000/users/detail 登录(POST):http://localhost:3000/users/login

m 用户访问 A 网站,登录后调用了 登录接口,接口返回了 cookie 设置在 A 网站域名中。
此时 m 用户,忍不住诱惑,点进了一个恶意链接 http://localhost:5500

这个恶意网站分别对 GET 和 POST 方法对两个接口进行攻击。

  1. 获取用户详细信息(GET) ```html


2. 更新用户信息(POST)
```html
<form action="http://localhost:3000/users/update" method="post" id="test">
  <input style="display:none;" type="text" name="name" value="kon"><br>
  <input style="display:none;" type="text" name="password" value="123456">
</form>
<script>
  function btn2() {
    const f = document.getElementById('test');
    f.submit();
  }
</script>

为啥 Form 表单可以跨域啊,反直觉是不是? 答: 因为原页面用 form 提交到另一个域名之后,原页面的脚本无法获取新页面中的内容。 所以浏览器认为这是安全的。 而 AJAX 是可以读取响应内容的,因此浏览器不能允许你这样做。

后台可以设置接口,要求请求头的字段Content-Type: application/json来限制数据格式,防止 form 表单的 CSRF。

  1. 链接类型(GET)

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

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

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

检查 CSRF 漏洞

假设场景为登录页面,登录之后会有 cookie,以后发起的请求会携带 cookie

那么我登录完,找那个接口直接通过浏览器 url 访问。
由于浏览器有缓存了该网站的 cookie ,那么去访问接口时,浏览器会自动携带这个 cookie,看能不能直接拿到数据。如果拿得到,就存在 CSRF 漏洞,如果拿不到,就不存在。

我访问了掘金的一个接口,直接拿到了这些内容,这个接口就有 CSRF 漏洞。

所以不是每个接口都必须做 CSRF 的防御的,只是在一些必要的接口,比如修改密码、转账、修改个人信息等等重要接口,就需要。 像这个掘金的年度报告接口,随便啊,拿到之后,也只是访问了年度报告的网页,它需要勾选同意,多加一个 POST 请求,验证身份,而这个接口就有做 CSRF 防御 [https://api.juejin.cn/event_api/v1/annual/annual_summary?aid=2608](https://api.juejin.cn/event_api/v1/annual/annual_summary?aid=2608)

image.png
image.png
访问掘金的另一个接口,拿不到内容,这个接口有做 CSRF 的防御
image.png

如何自动检测 CSRF 漏洞

在另外一个网页,发起对应接口的请求,可以成功,这个就成功了。
那么有什么自动化的工具,可以帮我们发现这些 CSRF 漏洞呢?

  1. BF —— Burf Suite Professional

    1. 先设置一下,把浏览器的请求,代理到 BF 中
    2. 开启 BF 的监听
    3. 选中监听到的接口,BF 自带了创建 CSRF 攻击的设置。engagement Tool ——> generate CSRF toC
    4. 把 BF 生成的 HTML 代码执行
  2. 腾讯云漏洞扫描服务

CSRF 攻击的防御措施

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

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

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

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

  • 阻止不明外域的访问
    • 同源检测
    • Samesite Cookie
  • 提交时要求附加本域才能获取的信息
    • CSRF Token
    • 双重 Cookie 验证

1. 同源检测

既然 CSRF 大多来自第三方网站,那么我们就直接禁止外域(或者不受信任的域名)对我们发起请求。

在HTTP协议中,每一个异步请求都会携带两个 Header 字段,用于标记来源域名:

  • Origin Header
  • Referer Header

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

Referer Check 不仅能防范 CSRF 攻击,另一个应用场景是“防止图片盗链”。

Origin Header

在部分与 CSRF 有关的请求中,请求的 Header 中会携带 Origin 字段。

字段内包含请求的域名(不包含 path 及 query)

如果 Origin 存在,那么直接使用 Origin 中的字段确认来源域名就可以

但是Origin在以下两种情况下并不存在:

  • IE11 同源策略: IE11 不会在跨站 CORS 请求上添加 Origin 标头,Referer 头将仍然是唯一的标识。
    • 因为IE11对同源的定义和其他浏览器有不同,有两个主要的区别,可以参考MDN Same-origin_policy
  • 302 重定向: 在 302 重定向之后 Origin 不包含在重定向的请求中,因为 Origin 可能会被认为是其他来源的敏感信息。
    • 对于 302 重定向的情况来说,都是定向到新的服务器上的 URL,因此浏览器不想将 Origin 泄漏到新的服务器上

Referer Header

根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,记录了该 HTTP 请求的来源地址。

  • 对于 Ajax 请求,图片和 script 等资源请求,Referer 为发起请求的页面地址。
  • 对于页面跳转,Referer 为打开页面历史记录的前一个页面地址。

使用验证 Referer 值的方法,就是把安全性都依赖于浏览器来保障,从理论上来讲,这样并不是很安全。
在部分情况下,攻击者可以隐藏,甚至修改自己请求的 Referer。

2014 年,W3C 的 Web 应用安全工作组发布了 Referrer Policy 草案,对浏览器该如何发送 Referer 做了详细的规定。截止现在新版浏览器大部分已经支持了这份草案,我们终于可以灵活地控制自己网站的 Referer 策略了。
新版的 Referrer Policy 规定了五种 Referer 策略:

  • No Referrer
  • No Referrer When Downgrade
  • Origin Only
  • Origin When Cross-origin
  • Unsafe URL

之前就存在的三种策略

  • never
  • default
  • always

在新标准里换了个名称。他们的对应关系如下:
XSS 和 CSRF - 图7
设置 Referrer Policy 的方法有三种:

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

比如我们下面的 img 标签这样写,那么这个请求发起的攻击将不携带 Referer

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

另外在以下情况下Referer没有或者不可信:

  1. IE6、7下使用 window.location.href=url 进行界面的跳转,会丢失 Referer。
  2. IE6、7下使用 window.open,也会缺失 Referer。
  3. HTTPS 页面跳转到 HTTP 页面,所有浏览器 Referer 都丢失。
  4. 点击 Flash 上到达另外一个网站的时候,Referer 的情况就比较杂乱,不太可信。

问答环节
  1. 诶,那我们不能获取到 Origin 和 Referer,怎么办?

答:
没有 Origin 和 Referer 的请求,说明是黑客,不给他不就完了?对吗?
不对哦,如果是我们直接把接口链接,用浏览器访问,这时候后台是拿不到 referer 和 origin 的。
这个时候也不给数据吗?看接口涉及是的什么业务,会不会泄露隐私而定吧。

  1. 同源检测是检测第三方网站的,如果攻击是从本站发起的怎么办?

    如果攻击者有权限在本域发布评论(含链接、图片等,统称UGC),那么它可以直接在本域发起攻击,这种情况下同源策略无法达到防护的作用。

答:这时需要利用别的方法,来进行用户鉴别,设计一套合理的用户的权限鉴定方案。

2. SameSite

可以对 Cookie设置SameSite属性。该属性设置Cookie不随跨域请求发送,可以大幅度减少 CSRF攻击。

SameSite 的兼容性需要考虑:点击查看 SameSite 兼容性(Can I use)

它可以设置三个属性:

  • Strict
  • Lax
  • None
    2.1 Strict
    Strict 最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。

    这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。

Set-Cookie: CookieName=CookieValue; SameSite=Strict;

2.2 Lax

Lax 规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。

请求类型 示例 正常情况 Lax
链接 <a href="..."></a> 发送 Cookie 发送 Cookie
预加载 <link rel="prerender" href="..."/> 发送 Cookie 发送 Cookie
GET 表单 <form method="GET" action="..."> 发送 Cookie 发送 Cookie
POST 表单 <form method="POST" action="..."> 发送 Cookie 不发送
iframe <iframe src="..."></iframe> 发送 Cookie 不发送
AJAX $.get("...") 发送 Cookie 不发送
Image <img src="..."> 发送 Cookie 不发送

2.3 None

Chrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为 None。
不过,前提是必须同时设置Secure属性(即 Cookie 只能通过 HTTPS 协议发送),否则None无效。

下面是无效设置

Set-Cookie: widget_session=abc123; SameSite=None

下面是有效设置

Set-Cookie: widget_session=abc123; SameSite=None; Secure

3. 双重 Cookie 验证

利用 CSRF 攻击不能直接拿到到用户 Cookie 的特点,我们可以要求 Ajax 和表单请求携带一个 Cookie 中的值。

设置流程如下:

  1. 在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串
    1. 例如csrfcookie=v8g9e4ksfhw
  2. 在前端向后端发起请求时,取出 Cookie,并添加到 URL 的参数中
    1. POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw
  3. 后端接口验证 Cookie 中的字段与 URL 参数中的字段是否一致,不一致则拒绝。

当然,此方法并没有大规模应用,其在大型网站上的安全性还是没有 CSRF Token 高,原因我们举例进行说明。

由于任何跨域都会导致前端无法获取 Cookie 中的字段(包括子域名之间),于是发生了如下情况:

  • 如果用户访问的网站为www.a.com,而后端的 api 域名为api.a.com
    • 那么在www.a.com下,前端拿不到api.a.com的 Cookie,也就无法完成双重 Cookie 认证。
  • 于是这个认证 Cookie 必须被种在 a.com 下,这样每个子域都可以访问。
  • 任何一个子域都可以修改 a.com 下的 Cookie。
  • 某个子域名存在漏洞被 XSS 攻击(例如upload.a.com)。
    • 虽然这个子域下并没有什么值得窃取的信息。但攻击者修改了a.com下的 Cookie。
  • 攻击者可以直接使用自己配置的 Cookie,对 XSS 中招的用户再向www.a.com下,发起 CSRF 攻击。

优点

用双重 Cookie 防御 CSRF 的优点:

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

    缺点
  • Cookie 中增加了额外的字段。

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

    4. 添加 token 验证

    CSRF 攻击之所以能够成功,是因为服务器误把 攻击者发送的请求 当成了 用户自己的请求。
    那么我们可以要求所有的用户请求都携带一个 CSRF 攻击者 无法获取到的 Token。
    服务器通过校验请求是否携带正确的 Token,来把 正常的请求 和 攻击的请求 区分开,也可以防范 CSRF 的攻击

原理

防护策略分为四个步骤:

  1. 用户访问时,服务器生成一个 token 返回给客户端。

该 token 通过加密算法对数据进行加密,一般 token 都包括随机字符串和时间戳的组合。

随机字符串,代表的是当前会话,要确保它的唯一性。 这里的 token 是指某个会话的 token,不是身份验证的 token。

显然在提交时 token 不能再放在 Cookie 中了,否则又会被攻击者冒用。 因此,为了安全起见 token 最好还是存在服务器的 Session 中(一个存放会话信息的数据结构 / 数据库)

  1. 将 token 输出到页面中 或 存在 localStorage

    方式一:存在需要标签 在每次页面加载时,使用 JS 对于 DOM 中所有的 a 和 form 标签加入 token (比如 data-token) 这样可以解决大部分的请求,但是对于在页面加载之后动态生成的 HTML 代码,这种方法就没有作用,还需要程序员在编码时手动添加 token

方式二:存在 localstorage

  1. 提交请求时,携带上 token

    方式一: GET 请求的话,可以在 url 上携带 token。如:http://url?csrftoken=tokenvalue

方式二: POST 请求的话,可以在 form 表单最后加上 <input type="hidden" name="csrftoken" value="tokenvalue"/>

方式三: 每次请求,获取 token,设置在请求头的 Authorization

  1. 服务器校验 token

服务端解密出 token,检验是哪次会话,是否过期等。
检验通过,则在 Session 中找出相应的会话信息,再利用会话信息查找相应数据,返回给客户端。

分布式校验

在大型网站中,使用 Session 存储 token 会带来很大的压力。由于使用Session存储,读取和验证CSRF Token会引起比较大的复杂度和性能问题。
目前很多网站采用Encrypted Token Pattern方式(例如jwt)。这种方法的 token 是一个计算出来的结果,而非随机生成的字符串。
这样在校验时无需再去读取存储的 token,只用再次计算一次即可。

在 token 解密成功之后,服务器可以访问解析值,token 中包含的 UserID 和 时间戳 将会被拿来被验证有效性,将 UserID 与当前登录的 UserID 进行比较,并将时间戳与当前时间进行比较。

5. 使用验证码

验证码被认为是对抗CSRF攻击最简洁有效的方法。

从上述攻击示例看出,CSRF攻击往往是再用户不知情的情况下构造了网络请求。而验证码回强制客户必须于应用进行交互,才能完成最终请求。因为通常情况下,验证码可以和那后的遏制 CSRF攻击。

然而处于用户考虑,并不能在网站的所有操作上都加上验证码。所以他只能作为预防CSRF的一种辅助手段。

总结防御措施

简单总结一下上文的防护策略:

  • CSRF自动防御策略:同源检测(Origin 和 Referer 验证)。
  • CSRF主动防御措施:Token 验证 或者 双重 Cookie 验证 以及配合 Samesite Cookie。
  • 保证页面的幂等性,后端接口不要在 GET 页面中做用户操作。

    XSS 和 CSRF 的区别

    image.png
    图:左侧是 CSRF 攻击,右侧是 XSS 攻击
  1. CSRF 攻击发起,一般是在一个恶意网站发起的,所以叫跨站
    1. 不需要注入脚本,直接利用你在 正常网站的状态,用跨域标签去发起请求,而不能直接拿到你的 cookie
  2. XSS 攻击(除了基于 DOM 类型的),是访问的服务器,返回了被注入的恶意代码

    参考资料

    《【基本功】 前端安全系列之二:如何防止CSRF攻击?》
    《Cookie 的 SameSite 属性》