XSS 与 CSRF 的攻防
前言
¶本文主要参考了美团技术团队的系列文章:
XSS 攻击
¶XSS[1] (Cross-site Scripting) 跨站脚本攻击,是一种代码注入攻击。攻击者通过在 web 页面中插入浏览器上可执行的恶意代码,在用户浏览网页时恶意代码会被浏览器执行,从而完成攻击。
根据攻击的来源,XSS 攻击可分为存储型、反射型和 DOM 型三种:
存储型 XSS 攻击步骤
- 攻击者将恶意代码提交到目标网站的数据库中(如在输入框中输入
"><script>alert('xss')</script>
); - 用户访问目标网站,服务端从数据库中取出包含恶意代码的数据,未经正确转义就通过模板引擎渲染成 HTML 返回;
- 用户浏览器渲染接收到的 HTML,混在其中的恶意代码得到执行。
- 攻击者将恶意代码提交到目标网站的数据库中(如在输入框中输入
反射型 XSS 攻击步骤
- 攻击者构造出特殊的 url,其中包含恶意代码(如
https://example.com/?keyword=%3Cscript%3Ealert('xss')%3C/script%3E
); - 用户访问携带恶意代码的 URL,服务端从 URL 中取出含恶意代码的参数,未经正确转义就通过模板引擎渲染成 HTML 返回;
- 用户浏览器渲染接收到的 HTML,混在其中的恶意代码得到执行。
- 攻击者构造出特殊的 url,其中包含恶意代码(如
DOM 型 XSS 攻击步骤
- 攻击者构造出特殊的 url,其中包含恶意代码(如
https://example.com/?to=javascript%3Aalert('xss')
); - 用户访问携带恶意代码的 URL,前端从 URL 中取出含恶意代码的参数,传入特殊的 HTML 属性中(如
<a href="javascript:alert('xss')">link</a>
),在接下来的事件触发中(如点击前一个括号中的链接)恶意代码得到执行。
- 攻击者构造出特殊的 url,其中包含恶意代码(如
在 web 页面中,脚本注入有很多方法,常见的有:
构造特殊的 url:有些网站会从 url 中拉取参数直接进行字符串拼接;
特殊的 HTML 属性:如在
href
、src
属性中,包含javascript:
等可执行代码;事件回调函数(如
onload
、onclick
)中注入恶意代码;在标签属性中包含
"
或>
,提前关闭属性甚至合法的标签,从而注入额外的属性甚至 HTML 标签。
在早期前后端未分离时,很流行使用 JSP 等模板引擎生成 HTML,很容易忘记对用户输入的数据进行转义,直接通过模板引擎进行字符串拼接,从而产生 XSS 漏洞:
script
标签xss/example1.jsp1<div>keyword: <%= getParameter("keyword") %></div>如上是一段 JSP 代码,如果攻击者分享了一个 url,其内容为
https://example.com/?keyword=%3Cscript%3Ealert('xss')%3C/script%3E
,则渲染结果为:xss/example1.html1<div>keyword: <script>alert('xss')</script></div>url 中包含的 js 代码得到执行。
内联脚本
javascript:
(这段字符浏览器不区分大小写)xss/example2.jsp1<a href="<%= getParameter("to") %>">跳转...</a>同样地,对于面面这段 JSP 代码,若 url 为
https://example.com/?to=javascript:alert('xss')
,则渲染结果为:xss/example2.html1<a href="javascript:alert('xss')">跳转...</a>
XSS 攻击的预防
¶XSS 攻击有如下特点:
- 攻击者构造并提交恶意代码
- 浏览器执行恶意代码
我们可以针对这些特点进行防御。
输入过滤
¶由于网络服务中所有请求都可以绕过前端直接向服务端发起,因此仅在浏览器端中做输入过滤是不能起到防范xss的效果的。但是后端做输入过滤同样存在问题,原因是不同的客户端及其渲染框架对于要接受的数据的转义规则存在差别,而服务端要做过滤只能选择某一种转义规则,众口难调。贸然进行转义可能会丢失原意。
比如一个合法用户提交的内容为 a < b
,后端在写入数据前,将其转义成 a < b
,这在纯 HTML 中渲染是有效的,但如果它是作为一段 url 内容则这个转义会破坏了原意。此外,对于 React / Vue 等现代前端框架,在渲染内容时需要的是原始的字符串内容,转义后的内容无法正确展示。
纯前端渲染
¶采用前后端分离的开发模式,前端使用 React / Vue 等现代前端框架进行开发,以 React 为例:
在不使用
dangerouslySetInnerHTML
属性渲染数据时,其内部逻辑天然地对 HTML 进行了严格的转义。在代码规范中严禁内联 javascript 的事件注册方式,统一通过 React / jsx 提供的 Event Handlers 进行定义(在原始的 html / js 项目中,可以用
.addEventListener
方法进行事件注册) 函数来注册事件回调函数。
其它防御策略
¶Content Security Policy 严格的 CSP 在 XSS 的防范中可以起到以下的作用:
- 禁止加载外域代码,防止复杂的攻击逻辑。
- 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
- 禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
- 禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
- 合理使用上报可以及时发现 XSS,利于尽快修复问题。
CSRF 攻击
¶CSRF (Cross-site Request Forgery) 跨站请求伪造,是通过诱导受害者访问第三方网站,并在第三方网站中盗用用户在目标网站上的身份(Cookie)发送跨站请求进行攻击的。
- 受害者登录
a.com
- 攻击者引诱受害者访问
danger.com
(攻击者可以利用或控制的站点) - 在
danger.com
中,向a.com
发起一个请求(浏览器默认会携带a.com
的 Cookie) a.com
接收到请求,对请求进行验证(校验 Cookie),确认是受害者的凭证后执行请求
发送跨站请求有多种方式:
图片地址:浏览器在渲染页面时会自动访问
img
元素中指定的src
地址来加载图片,它本质上是一个GET
请求,如将此地址设置成<img src="https://bank.com/withdraw?amount=100&to=hacker">
,则在浏览器尝试加载此“图片”时,一次恶意的跨站请求就完成了。链接地址:需要诱导用户点击才会触发,如
<a href="https://bank.com/withdraw?amount=100&to=hacker" />
提交表单:此攻击方式比较严格,通常需要黑客完全控制第三方站点,可以在其上执行自己的 javascript 脚本,如:
csrf/example1.html12345678910<formid="hack-form"action="https://bank.com/withdraw"method="POST"><input type="hidden" name="amount" value="1000" /><input type="hidden" name="to" value="hacker" /></form><script>document.getElementById('hack-form').submit()</script>
CSRF 攻击的防护
¶CSRF 攻击有如下特点:
攻击通常发生在第三方网站。
攻击通过冒用受害者的登陆凭证发起恶意请求完成,整个阶段中不能获得用户的登录凭证。
跨站请求十分容易达成,图片、超链接、表单提交,在一些可以发图、链接的第三方平台中可以直接嵌入恶意请求(如在一些支持 Markdown 语法的评论框中内嵌一个恶意的请求地址作为 url 的图片)。
如果被攻击的目标站点中有上述容易被利用的功能,则攻击可以直接在本域下进行,此时攻击可以绕过同源策略的防御机制。
我们可以针对这些特点进行防御。
同源检测
¶CSRF 攻击大多是利用第三方站点发起恶意请求,因此直接拒绝来自第三方站点的请求进行规避(此方法对于本站下发起的 CSRF 攻击无效)。
如何判断请求是否来自外域:
检查 HTTP Header 中的
Origin
字段Origin 在以下两种情况下不存在:
IE11 同源策略:IE11 不会在 CORS 请求上添加 Origin 字段。参见 https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#IE_Exceptions
302 重定向:在 302 重定向之后的请求中,请求头上不包含 Origin 字段。
检查 HTTP Header 中的
Referer
[2] 字段Referer 字段记录了请求的来源地址:
- 对于 ajax 请求以及图片、脚本等资源请求,Referer 为发起请求的页面地址
- 对于页面跳转,Referer 为页面历史记录中的上一个页面地址
Referrer-Policy: no-referrer
: 整个 Referer 首部会被移除。访问来源信息不随着请求一起发送。Referrer-Policy: no-referrer-when-downgrade
: (默认值)在没有指定任何策略的情况下用户代理的默认行为。在同等安全级别的情况下,引用页面的地址会被发送(HTTPS->HTTPS),但是在降级的情况下不会被发送 (HTTPS->HTTP)。Referrer-Policy: origin
: 在任何情况下,仅发送文件的源作为引用地址。例如 https://example.com/page.html
会将https://example.com/
作为引用地址。Referrer-Policy: origin-when-cross-origin
: 对于同源的请求,会发送完整的 url 作为引用地址,但是对于非同源请求仅发送文件的源。Referrer-Policy: same-origin
: 对于同源的请求会发送引用地址,但是对于非同源请求则不发送引用地址信息。Referrer-Policy: strict-origin
: 在同等安全级别的情况下,发送文件的源作为引用地址(HTTPS->HTTPS),但是在降级的情况下不会发送 (HTTPS->HTTP)。Referrer-Policy: strict-origin-when-cross-origin
: 对于同源的请求,会发送完整的URL作为引用地址;在同等安全级别的情况下,发送文件的源作为引用地址(HTTPS->HTTPS);在降级的情况下不发送此首部 (HTTPS->HTTP)。Referrer-Policy: unsafe-url
: 无论是同源请求还是非同源请求,都发送完整的 URL(移除参数信息之后)作为引用地址。CAUTION这项设置会将受 TLS 安全协议保护的资源的源和路径信息泄露给非安全的源服务器。进行此项设置的时候要慎重考虑。
CSRF Token
¶前文中提到 CSRF 攻击是利用了浏览器在发送请求时默认携带对应站点的 Cookie 的机制完成的,如果服务器要求所有的请求都需要携带一个 token
,且该 token
不存在服务器上,则校验此 token
就可以完全防御 CSRF 攻击了。
使用此方法需要在所有的请求中携带一个固定的 token
,可以将其写入在 html
的某个 meta
元素中,然后在发起请求时通过 js 查询此 token
;或者在服务端渲染页面时将此 token
注入到链接(站点链接,非用户填入的链接)和表单中。此外,还可以将
token
写入 HTTP Header 中,如使用 axios 请求库时,可以设置默认的 HTTP Headers。
由于此 token
是为了防止 CSRF 攻击存在的,仅用来校验请求是否从可信源中发出,而无需在服务器端废止它(仅设置一个过期时间让它自然失效即可),故可以使用 jwt
来实现,将用户名写进 jwt
中即可防止攻击者伪造 token
。这样服务器端就无需存储额外的信息了,仅需通过算法进行校验就可以完成验证了。
其它防御策略
¶双重 Cookie: 在 Cookie 中写入一个随机数(作为 CSRF Token),之后所有的请求中手动在请求参数中携带此随机数,服务器端通过检查 Cookie 中的随机数和请求中的随机数是否一致进行检验。
此方法和 CSRF Token 差不多,换汤不换药
Samesite Cookie: 服务器在 http 请求头上写入
Set-Cookie: xxxxx; Samesite=Strict
表示此 Cookie 仅限同站下使用,从根源上杜绝了 CSRF 攻击。
Related
¶