xss-test
测试代码
为了代码简洁,直接使用 CDN 的方式引入Vue3, 假设输入框里输入的是后端传过来的数据,这个数据就是要使用 v-html 渲染的模板字符串
<!DOCTYPE html>
<head>
<title>xss测试</title>
</head>
<body>
<div id="app">
<p v-html="inputStr"></p>
<input type="text" v-model="inputStr">
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const { createApp } = Vue;
createApp({ data() { return { inputStr: "" }; }, }).mount('#app');
</script>
</body>
</html>
第一个测试
我们先测试一个正常(无xss攻击)的例子
<div>Hello World</div>
效果:
结论:测试代码按照预期正常显示节点
第二个测试
我们再测试一个有带有 script 脚本标签的例子,看他是否会执行该脚本。
<script>alert(1)</script>
效果:
结论:节点上存在 script 标签,但是脚本没有被执行。
思考:既然脚本没有被执行,那是不是就能说明 v-html 自动转义的功劳或者是浏览器可以对 XSS 进行有效防御呢? 我们不妨在多测一些其他更高级的 XSS 攻击。
第三个测试
利用 a 标签的 onclick 属性嵌入 XSS。
<a href="#" onclick="alert(1)">点击我</a>
效果:
点击之后:
结论:这里形成了一个a链接,当我们点击的时候触发了alert,这足以说明 v-html 并不能过滤掉 XSS 攻击;我们再测试一种。
第四个测试
利用 img 标签的 onerror 属性嵌入 XSS。
<img src="#" onerror="alert(1)">
效果:
结论:这里直接触发 alert ,再次说明,如果后端传过来的模板字符串不进行 XSS 过滤,将会存在被恶意脚本攻击的隐患。
思考:那为什么第一种不行呢?
查阅后发现,这个不是vue的限制,而是html5中的规定。 html5 中为了安全起见,不会执行 innerHTML 中插入的
<script>的代码。
所以,如果想给 v-html 赋值
<script>的代码,虽然能看到在 dom 中成功展示,但却不会执行<script>中的 js 代码的。那万一我确实有这种需求那该怎么办呢? 虽然这种需求少之又少。
在不使用任何库的情况下有一个方法:
// 大概是先写段代码,创建script标签,然后append到指定标签即可。
// html
<h2 ref="testxss"></h2>
// js
const script = document.createElement('script')
script.innerHTML = document.querySelect('input').value
this.$refs.testxss.append(script)
什么是 XSS
到这里,相信大家不了解 XSS 的都大概知道了什么是 XSS 。下面我在简简简单的介绍一下 XSS 。
XSS 全称是 Cross Site Scripting(跨站脚本), 为了与 “CSS” 区分开,故简称 XSS 。XSS 攻击是页面被注入了恶意的代码,从而让用户浏览页面时利用恶意脚本对用户实施攻击的一种手段。
XSS 不是仅仅是我这次演示的这些这么简单。如今脚本语言几乎无所不能。
- 如果脚本执行的不只是 alert(1);
- 而是通过“document.cookie”盗取 Cookie ,然后通过 XMLHttpRequest 或者 Fetch 加上 CORS 功能将数据发送给恶意服务器,恶意服务器拿到用户的 Cookie 信息之后就可以模拟用户登录,然后进行其他操作了;
- 而是使用 “addEventListener" 来监听键盘事件,比如可以获取用户输入的个人信息,将其发送到恶意服务器,黑客掌握这些信息之后,又可以进行其他操作了;
- 而是通过修改 DOM 伪造假的登录窗口,用来欺骗用户输入用户名和密码等个人信息;
- 还可以再页面内生成浮窗广告,影响用户体验。
- .........
只有更清楚的认识到 XSS 的危害,才会有意识地去防范它! --- wzk
XSS 的种类
根据攻击的来源,XSS 攻击可分为存储型、反射型和 DOM 型三种。
存储型 XSS 的攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中。
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
反射型 XSS 的攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
这种攻击常见攻击者通过 qq 群发 URL 链接诱导用户点击链接。
DOM 型 XSS 的攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL。
- 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
基于 DOM 的 XSS 攻击是不牵涉到服务器的。攻击者通过各种手段将恶意脚本注入到用户页面中,比如通过网络劫持( Wifi 路由器劫持、本地恶意软件)在页面传输过程中修改 HTML 页面的内容(例如广告)。
备注:https 只能增加攻击难度,不能防止所有场景的基于 DOM 的 XSS 攻击
XSS 攻击的预防
XSS 攻击有两大要素:
- 攻击者提交恶意代码。
- 浏览器执行恶意代码。
XSS 最终都要通过浏览器执行,所以前端工程师是最后的把关者,预防这两种漏洞,有两种常见做法:
- 改成纯前端渲染,把代码和数据分隔开。
- 对 HTML 做充分转义。
对于充分的转义:
错误做法:
所有要插入到页面上的数据,都要通过一个敏感字符过滤函数的转义,过滤掉通用的敏感字符后,就可以插入到页面中。
正确做法:
不同的上下文,如 HTML 属性、HTML 文字内容、HTML 注释、跳转链接、内联 JavaScript 字符串、内联 CSS 样式表等,所需要的转义规则不一致,针对不同的上下文调用不同的转义规则。
js-xss
js-xss是一个用于对用户输入的内容进行过滤,以避免遭受 XSS 攻击的模块。主要用于论坛、博客、网上商店等等一些可允许用户录入页面排版、格式控制相关的 HTML 的场景。
特性:
- 可配置白名单控制允许的 HTML 标签及各标签的属性;
- 通过自定义处理函数,可对任意标签及其属性进行处理;
由于未知原因使用在线引入的方法失效,所以进入官网直接下载其源码。当然 node 环境可以直接通过 npm 下载使用。
基础用法
测试一:
// 直接使用filterXSS函数
filterXSS(this.inputStr)
输入框输入:
<a onclick="alert('攻击你')">连接</a>
效果:
结论:链接无法点击、a 节点的 onclick 直接干掉。
测试二( filterXSS() 方法可以自定义配置过滤规则 ):
// 设置白名单,只允许a标签,该标签只允许href, title, target这三个属性
var options = { whiteList: { a: ["href", "title", "target"] } };
// 第二个形参填入自定义规则
filterXSS(this.inputStr, options);
输入框输入
<a href="#" onclick="alert('攻击你')"><i>大家好</i></a>
将被过滤为 :
<a href="#">大家好</a>
更多使用方法请看官网:jsxss.com/zh/starter/…
js-xss 做了什么事
getDefaultWhiteList()
首先打开上面的源码地址我们首先看到 getDefaultWhiteList() 方法:
function getDefaultWhiteList() {
return {
a: ["target", "href", "title"],
abbr: ["title"],
address: [],
···
···
···
tt: [],
u: [],
ul: [],
video: ["autoplay", "controls", "loop", "preload", "src", "height", "width"]
};
}
getDefaultWhiteList()方法return出默认的所有标签名,如果用户没有自定义options参数与配置,那xss()将默认处理所有的标签属性;
接下来的方法:
// 以下为函数方法的作用,FN:后面为函数方法名称
FN: onTag() // 自定义匹配到标签时的处理方法,默认不做处理;
FN: onIgnoreTag() // 自定义匹配到不在白名单上的标签时的处理方法,默认不做处理;
FN: onTagAttr() // 自定义匹配到标签的属性时的处理方法,默认不做处理;
FN: onIgnoreTagAttr() // 自定义匹配到不在白名单上的标签时的处理方法,默认不做处理;
FN: escapeHtml() // 把所有‘< >’ 处理为 “< ">”
FN: safeAttrValue() // 处理 href、src、style、url等属性,如不规范则返回空
核心的正则表达式
接下来就是js-xss最核心的正则部分了,xss()过滤规则主要是靠下面13个正则表达式匹配之后进行处理。
// 匹配 尖括号
var REGEXP_LT = /</g;
var REGEXP_GT = />/g;
// 匹配 双引号
var REGEXP_QUOTE = /"/g;
var REGEXP_QUOTE_2 = /"/g;
// 匹配 大小写&#数字 全局换行忽略大小写搜索
var REGEXP_ATTR_VALUE_1 = /&#([a-zA-Z0-9]*);?/gim;
// 匹配 : &newline;
var REGEXP_ATTR_VALUE_COLON = /:?/gim;
var REGEXP_ATTR_VALUE_NEWLINE = /&newline;?/gim;
// 匹配 ‘/*’、‘*\’ 全局换行搜索
var REGEXP_DEFAULT_ON_TAG_ATTR_3 = /\/\*|\*\//gm;
// 匹配javascript和vscript和livescript
var REGEXP_DEFAULT_ON_TAG_ATTR_4 = /((j\s*a\s*v\s*a|v\s*b|l\s*i\s*v\s*e)\s*s\s*c\s*r\s*i\s*p\s*t\s*|m\s*o\s*c\s*h\s*a)\:/gi;
// 匹配 data
var REGEXP_DEFAULT_ON_TAG_ATTR_5 = /^[\s"'`]*(d\s*a\s*t\s*a\s*)\:/gi;
// 匹配 "'` data imge
var REGEXP_DEFAULT_ON_TAG_ATTR_6 = /^[\s"'`]*(d\s*a\s*t\s*a\s*)\:\s*image\//gi;
// 匹配 expression(
var REGEXP_DEFAULT_ON_TAG_ATTR_7 = /e\s*x\s*p\s*r\s*e\s*s\s*s\s*i\s*o\s*n\s*\(.*/gi;
// 匹配 url(
var REGEXP_DEFAULT_ON_TAG_ATTR_8 = /u\s*r\s*l\s*\(.*/gi;
紧接着通过exports.将所有方法暴露至全局:
exports.whiteList = getDefaultWhiteList();
exports.getDefaultWhiteList = getDefaultWhiteList;
exports.onTag = onTag
···
···
···
exports.cssFilter = defaultCSSFilter;
exports.getDefaultCSSWhiteList = getDefaultCSSWhiteList;
FilterXSS()方法
function FilterXSS(options) {
options = shallowCopyObject(options || {});
// 判断用户是否传入配置如未传入则使用默认配置
if (options.stripIgnoreTag) {
if (options.onIgnoreTag) {
console.error(
'Notes: cannot use these two options "stripIgnoreTag" and "onIgnoreTag" at the same time'
);
}
options.onIgnoreTag = DEFAULT.onIgnoreTagStripAll;
}
options.whiteList = options.whiteList || DEFAULT.whiteList;
options.onTag = options.onTag || DEFAULT.onTag;
options.onTagAttr = options.onTagAttr || DEFAULT.onTagAttr;
options.onIgnoreTag = options.onIgnoreTag || DEFAULT.onIgnoreTag;
options.onIgnoreTagAttr = options.onIgnoreTagAttr || DEFAULT.onIgnoreTagAttr;
options.safeAttrValue = options.safeAttrValue || DEFAULT.safeAttrValue;
options.escapeHtml = options.escapeHtml || DEFAULT.escapeHtml;
this.options = options;
if (options.css === false) {
this.cssFilter = false;
} else {
options.css = options.css || {};
this.cssFilter = new FilterCSS(options.css);
}
}
/**
* 启动进程,在FilterXSS.prototype注入方法
*
* @param {String} html
* @return {String}
*/
FilterXSS.prototype.process = function(html) {
// 兼容html内容
html = html || "";
html = html.toString();
if (!html) return "";
···
···
···
// 移除不可见字符
if (options.stripBlankChar) {
html = DEFAULT.stripBlankChar(html);
}
// 移除html注释
if (!options.allowCommentTag) {
html = DEFAULT.stripCommentTag(html);
}
// 是否过滤掉不在白名单中的标签
var stripIgnoreTagBody = false;
if (options.stripIgnoreTagBody) {
var stripIgnoreTagBody = DEFAULT.StripTagBody(
options.stripIgnoreTagBody,
onIgnoreTag
);
onIgnoreTag = stripIgnoreTagBody.onIgnoreTag;
}
// 处理html内容
var retHtml = parseTag(
html,
function(sourcePosition, position, tag, html, isClosing) {
···
···
···
var attrs = getAttrs(html); // 获取去除标签名后的内容
var whiteAttrList = whiteList[tag];
// 解析输入属性并返回已处理的属性
var attrsHtml = parseAttr(attrs.html, function(name, value) {
···
···
···
});
// 把处理过的标签+属性重新组合起来创建新的html标签
var html = "<" + tag;
if (attrsHtml) html += " " + attrsHtml;
if (attrs.closing) html += " /";
html += ">";
return html;
} else {
// call `onIgnoreTag()`
var ret = onIgnoreTag(tag, html, info);
if (!isNull(ret)) return ret;
return escapeHtml(html);
}
},
escapeHtml
);
// if enable stripIgnoreTagBody
if (stripIgnoreTagBody) {
retHtml = stripIgnoreTagBody.remove(retHtml);
}
return retHtml;
};
CSS过滤器
function FilterCSS (options) {
// 判断用户是否传入配置如未传入则使用默认配置
options = shallowCopyObject(options || {});
options.whiteList = options.whiteList || DEFAULT.whiteList;
options.onAttr = options.onAttr || DEFAULT.onAttr;
options.onIgnoreAttr = options.onIgnoreAttr || DEFAULT.onIgnoreAttr;
options.safeAttrValue = options.safeAttrValue || DEFAULT.safeAttrValue;
this.options = options;
}
// FilterCSS.prototype注入方法
FilterCSS.prototype.process = function (css) {
// 兼容各种奇葩输入
css = css || '';
css = css.toString();
if (!css) return '';
···
···
···
// 解析style并处理style样式
var retCSS = parseStyle(css, function (sourcePosition, position, name, value, source) {
var check = whiteList[name];
var isWhite = false;
if (check === true) isWhite = check;
else if (typeof check === 'function') isWhite = check(value);
else if (check instanceof RegExp) isWhite = check.test(value);
if (isWhite !== true) isWhite = false;
// 如果过滤后 value 为空则直接忽略
value = safeAttrValue(name, value);
if (!value) return;
···
···
···
});
return retCSS;
};