一、XSS
即 Cross Site Script (跨站脚本攻击),由于缩写与 CSS 冲突,故称 XSS
指的是攻击者在网络上注入恶意的客户端代码,通过恶意脚本堆客户端网页进行篡改,从而在用户浏览网页时,对用户浏览器进行控制 或者 获取用户隐私数据的一种攻击方式。
注入的恶意脚本一般包括:JS
,有时也会包含HTML
和Flash
XSS
攻击可以分为以下 3 类
- 反射型(非持久型)
- 存储型(持久型)
- 基于
DOM
她们的共同点为:将一些隐私数据如 cookie
、session
发送给攻击者,将受害者重定向到一个由攻击者控制的网站,在受害者的机器上进行一些恶意操作。
XSS 的类型
1. 反射型
用户通过 Web 客户端提交给服务端的数据,立刻用于解析和显示该用户的结果页面(数据没有在服务端存储)
如果提交的数据中含有恶意的脚本代码,而服务端没有经过编码转换或者过滤,就会形成 XSS 攻击,这种形式的 XSS 称为反射型 XSS
常见的通过浏览器地址栏输入的 HTTP GET请求参数 和 页面搜索框输入的 POST 查询内容 恶意用户通过构造含恶意脚本的 URL,发送到各种群、朋友圈、邮箱,诱导用户点击,获取点击用户的信息,达到攻击目的。
2. 存储型
用户通过Web客户端提交给服务端的数据,由服务端保存,然后永久显示在其他用户的页面上。
如果提交的数据中含有恶意的脚本代码,而服务端在存储前或展示前没有经过编码转换或者过滤,就会形成 XSS攻击,这种形式的 XSS 称为存储型 XSS
常见的场景是攻击者,在社区或论坛上,写下一篇包含恶意
JS
代码的文章或评论,文章或评价发表后,所有访问该文章或评论的用户,都会他们的浏览器中执行这段恶意的代码。
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 攻击的防御措施
现在主流的浏览器内置了防范 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 用户权限下的接口
- 跨站:B 域名网站
通过某种方式是什么方式?
- 方式一:黑客知道了 m 用户的账号密码,登录后,拿到 cookie
- 方式二:
- 在 m 用户登录了 A 网站之后,A 服务器把
set-Cookie
,然后 A 网站就会有 cookie,在 cookie 没有过期或者销毁之前,m 用户被诱导打开了 B 网站 - 这时 B 网站访问了 m 在 A 网站的接口,浏览器会自动携带上 m 的 cookie 发给 A 网站
- 在 m 用户登录了 A 网站之后,A 服务器把
浏览器为啥要带上 cookie 发给 A 呢,不是明明是在 B 网站访问的 A 服务器的接口吗,说好的浏览器的同源策略呢? 答:在 B 网站用 ajax 请求,浏览器当然不会把 m 用户在 A 网站的 cookie 发给 A 的服务器的,这符合同源策略。 但是呢,有允许跨域的标签啊,允许跨域的属性啊,利用这些,B 网站发送的请求,浏览器就会携带 m 用户的 cookie 发给 A 的服务器。
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 方法对两个接口进行攻击。
- 获取用户详细信息(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。
- 链接类型(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)
访问掘金的另一个接口,拿不到内容,这个接口有做 CSRF 的防御
如何自动检测 CSRF 漏洞
在另外一个网页,发起对应接口的请求,可以成功,这个就成功了。
那么有什么自动化的工具,可以帮我们发现这些 CSRF 漏洞呢?
BF —— Burf Suite Professional
- 先设置一下,把浏览器的请求,代理到 BF 中
- 开启 BF 的监听
- 选中监听到的接口,BF 自带了创建 CSRF 攻击的设置。
engagement Tool ——> generate CSRF toC
- 把 BF 生成的 HTML 代码执行
腾讯云漏洞扫描服务
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
在新标准里换了个名称。他们的对应关系如下:
设置 Referrer Policy 的方法有三种:
- 在 CSP 设置
- 页面头部增加 meta 标签
- 跨域标签增加
referrerpolicy
属性
比如我们下面的 img 标签这样写,那么这个请求发起的攻击将不携带 Referer
<img src="http://bank.example/withdraw?amount=10000&for=hacker" referrerpolicy="no-referrer">
另外在以下情况下Referer没有或者不可信:
- IE6、7下使用 window.location.href=url 进行界面的跳转,会丢失 Referer。
- IE6、7下使用 window.open,也会缺失 Referer。
- HTTPS 页面跳转到 HTTP 页面,所有浏览器 Referer 都丢失。
- 点击 Flash 上到达另外一个网站的时候,Referer 的情况就比较杂乱,不太可信。
问答环节
- 诶,那我们不能获取到 Origin 和 Referer,怎么办?
答:
没有 Origin 和 Referer 的请求,说明是黑客,不给他不就完了?对吗?
不对哦,如果是我们直接把接口链接,用浏览器访问,这时候后台是拿不到 referer 和 origin 的。
这个时候也不给数据吗?看接口涉及是的什么业务,会不会泄露隐私而定吧。
- 同源检测是检测第三方网站的,如果攻击是从本站发起的怎么办?
如果攻击者有权限在本域发布评论(含链接、图片等,统称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 中的值。
设置流程如下:
- 在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串
- 例如
csrfcookie=v8g9e4ksfhw
- 例如
- 在前端向后端发起请求时,取出 Cookie,并添加到 URL 的参数中
POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw
- 后端接口验证 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 的攻击
原理
防护策略分为四个步骤:
- 用户访问时,服务器生成一个 token 返回给客户端。
该 token 通过加密算法对数据进行加密,一般 token 都包括随机字符串和时间戳的组合。
随机字符串,代表的是当前会话,要确保它的唯一性。 这里的 token 是指某个会话的 token,不是身份验证的 token。
显然在提交时 token 不能再放在 Cookie 中了,否则又会被攻击者冒用。 因此,为了安全起见 token 最好还是存在服务器的 Session 中(一个存放会话信息的数据结构 / 数据库)
- 将 token 输出到页面中 或 存在 localStorage
方式一:存在需要标签 在每次页面加载时,使用 JS 对于 DOM 中所有的 a 和 form 标签加入 token (比如
data-token
) 这样可以解决大部分的请求,但是对于在页面加载之后动态生成的 HTML 代码,这种方法就没有作用,还需要程序员在编码时手动添加 token
方式二:存在 localstorage
- 提交请求时,携带上 token
方式一: GET 请求的话,可以在 url 上携带 token。如:
http://url?csrftoken=tokenvalue
方式二: POST 请求的话,可以在 form 表单最后加上
<input type="hidden" name="csrftoken" value="tokenvalue"/>
方式三: 每次请求,获取 token,设置在请求头的
Authorization
- 服务器校验 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 的区别
图:左侧是 CSRF 攻击,右侧是 XSS 攻击
- CSRF 攻击发起,一般是在一个恶意网站发起的,所以叫跨站
- 不需要注入脚本,直接利用你在 正常网站的状态,用跨域标签去发起请求,而不能直接拿到你的 cookie
- XSS 攻击(除了基于 DOM 类型的),是访问的服务器,返回了被注入的恶意代码
参考资料
《【基本功】 前端安全系列之二:如何防止CSRF攻击?》
《Cookie 的 SameSite 属性》