上篇文章这一次,完全理解XSS攻击讲解了XSS攻击的类型和预防方式,本篇文章咱们来看这个36039K的XSS-NPM库(你没有看错就是3603W次, 36039K次,36,039,651次,数据来自https://npm-stat.com),相信挺多小伙伴在项目中,也用到了这个库。javascript
话很少说,咱们来看~css
js-xss
是一个用于对用户输入的内容进行过滤,以免遭受 XSS 攻击的模块(什么是 XSS 攻击?)。主要用于论坛、博客、网上商店等等一些可容许用户录入页面排版、格式控制相关的 HTML 的场景。html
特性:前端
可配置白名单控制容许的HTML标签及各标签的属性;java
经过自定义处理函数,可对任意标签及其属性进行处理;node
让咱们来看看下面的数据:git
🥇 GitHub 3.8K Star; (数据日期:2020-12-30,数据来源:js-xss-github)angularjs
🥇 周下载量575,790次; (数据日期:2020-12-24 ~ 2020-12-30,数据来源:xss-npm)github
🥇 总下载量36,039,651次;(数据日期:2013-01-31 ~ 2020-12-30,数据来源:npm-stat.com)正则表达式
🥇 Teambition
🥇 前端乱炖
🥇 为知笔记
// 安装xss依赖 npm install xss // 引入xss模块 const xss = require("xss"); // 使用 xss()方法处理内容 const html = xss('<script>alert("xss");</script>'); console.log(html);
// 注意请勿将URL地址用于生产环境,能够保存在本地引入使用。 <script src="https://rawgit.com/leizongmin/js-xss/master/dist/xss.js"></script> // 使用 filterXSS()方法处理内容 <script> var html = filterXSS('<script>alert("xss");</scr' + 'ipt>'); console(html); </script>
在调用 xss()
或者filterXSS()
函数进行过滤时,可经过第二个参数来设置自定义规则:
options = {}; // 自定义规则 // 第二个形参填入自定义规则 html = xss('<script>alert("xss");</script>', options);
若是多处使用,但不想每次都传入一个 options
参数,能够建立一个 FilterXSS
实例;
options = {}; // 自定义规则 myxss = new xss.FilterXSS(options); // 之后直接调用 myxss.process() 来处理便可 html = myxss.process('<script>alert("xss");</script>');
经过options
对象中的 whiteList
来指定,格式为:{'标签名': ['属性1', '属性2']}
。不在白名单上的标签将被过滤,不在白名单上的属性也会被过滤。如下是示例:
// 只容许a标签,该标签只容许href, title, target这三个属性 var options = { whiteList: { a: ["href", "title", "target"] } }; // 使用以上配置后,下面的HTML // <a href="#" onclick="hello()"><i>你们好</i></a> // 将被过滤为 // <a href="#">你们好</a>
经过 onTag
来指定相应的处理函数。如下是详细说明:
function onTag(tag, html, options) { // tag是当前的标签名称,好比<a>标签,则tag的值是'a' // html是该标签的HTML,好比<a>标签,则html的值是'<a>' // options是一些附加的信息,具体以下: // isWhite boolean类型,表示该标签是否在白名单上 // isClosing boolean类型,表示该标签是否为闭合标签,好比</a>时为true // position integer类型,表示当前标签在输出的结果中的起始位置 // sourcePosition integer类型,表示当前标签在原HTML中的起始位置 // 若是返回一个字符串,则当前标签将被替换为该字符串 // 若是不返回任何值,则使用默认的处理方法: // 在白名单上: 经过onTagAttr来过滤属性,详见下文 // 不在白名单上:经过onIgnoreTag指定,详见下文 }
经过 onTagAttr
方法来指定相应的处理函数。如下是详细说明:
function onTagAttr(tag, name, value, isWhiteAttr) { // tag是当前的标签名称,好比<a>标签,则tag的值是'a' // name是当前属性的名称,好比href="#",则name的值是'href' // value是当前属性的值,好比href="#",则value的值是'#' // isWhiteAttr是否为白名单上的属性 // 若是返回一个字符串,则当前属性值将被替换为该字符串 // 若是不返回任何值,则使用默认的处理方法 }
更多详细的options
参数与配置建议查看官方文档:js-xss-README
下面让咱们来一块儿看看,js-xss
的库是怎么防止xss攻击的吧~
对应源码地址:dist/xss.js
下面的源码分析从上到下,你们能够打开上述地址,两个窗口对比查看效果
首先打开上面的源码地址咱们首先看到时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个正则表达式匹配以后进行处理。
话很少说,咱们就看看大名鼎鼎的xss库到底用了哪些正则吧~
// 匹配 尖括号 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;
若是你把上面的正则一个个去理解,相信你就会知道这个总下载量3000W的xss库到底针对哪些属性作了处理。
咱们继续往下看,是对相关内容特殊符号及各类特殊字符方法:
// 如下为函数方法的做用,FN:后面为函数方法名称 FN: escapeQuote() // 全部的 " 替换成 " FN: unescapeQuote() // 全部的 " 替换成 " FN: escapeHtmlEntities() // 处理Unicode编码 FN: escapeDangerHtml5Entities() // 处理: &newline;转换为 : 空 FN: clearNonPrintableCharacter() // 清除没法使用的字符 FN: friendlyAttrValue() // 处理特殊的字符,将它们变成可展现的字符 FN: escapeAttrValue() // 将尖括号<>和引号" 进行转义 FN: onIgnoreTagStripAll() // 删除全部不在白名单的标签 FN: StripTagBody() // 指定一个标签列表,若是标签不在标签列表中,则经过指定函数处理 FN: stripCommentTag() // 删除html注释 FN: stripBlankChar() // 删除不可见字符
紧接着经过exports.将全部方法暴露至全局:
exports.whiteList = getDefaultWhiteList(); exports.getDefaultWhiteList = getDefaultWhiteList; exports.onTag = onTag ··· ··· ··· exports.cssFilter = defaultCSSFilter; exports.getDefaultCSSWhiteList = getDefaultCSSWhiteList;
这里是将filterXSS()
方法建立并暴露至全局,filterXSS看起来很简洁,new 了 FilterXSS对象,具体FilterXSS对象是什么从哪里,咱们在后面再作介绍。
/** * @param {String} html * @param {Object} 配置对象{ whiteList, onTag, onTagAttr... } * @return {String} */ function filterXSS(html, options) { var xss = new FilterXSS(options); return xss.process(html); }
接下来针对不一样环境将filterXSS方法暴露至全局:
exports = module.exports = filterXSS; exports.filterXSS = filterXSS; exports.FilterXSS = FilterXSS; for (var i in DEFAULT) exports[i] = DEFAULT[i]; for (var i in parser) exports[i] = parser[i]; // 在浏览器上使用xss,输出filterxss'到全局变量 if (typeof window !== "undefined") { window.filterXSS = module.exports; } // 在WebWorker上使用xss,输出filterxss'到全局变量 function isWorkerEnv() { return typeof self !== 'undefined' && typeof DedicatedWorkerGlobalScope !== 'undefined' && self instanceof DedicatedWorkerGlobalScope; } if (isWorkerEnv()) { self.filterXSS = module.exports; } },{"./default":1,"./parser":3,"./xss":5}],3:[function(require,module,exports){ /**
接下来依旧是封装了不少处理的方法:
FN: getTagName() // 获取标签的属性 FN: isClosing() // 是否有结束标记 FN: parseTag() // 解析输入html并返回已处理的html FN: parseAttr() // 解析输入属性并返回已处理的属性 FN: findNextEqual() // 查找下一个空格,用于寻找标签内属性 FN: findBeforeEqual() // 向前寻找空格 FN: isQuoteWrapString() // 判断是不是被双引号或者单引号包裹的 FN: stripQuoteWrap() // 若是被双引号或者单引号包裹的去除引号,不然返回原值 FN: isNull() // 判断输入的是否为 `undefined` or `null` FN: getAttrs() // 获取去除标签名后的内容 FN: shallowCopyObject() // 浅拷贝方法
若是说上面的正则和各类封装的方法是炮弹的话,这个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; };
// 如下为函数方法的做用,FN:后面为函数方法名称 FN: getDefaultWhiteList() // 获取白名单值,返回true表示容许该属性,其余值均表示不容许 FN: safeAttrValue() // 若是被双引号或者单引号包裹的去除引号,不然返回原值
好了,以上就是所有的内容啦.
若有疑问,可在下方留言,会第一时间进行回复!
码字不易。若是以为本篇文章对你有帮助的话,但愿能能够留言点赞支持,很是感谢~
2021你那已经来啦,祝你们新年快乐,2021代码无bug~
我曾踏足山巅,也曾跌落谷底,二者都让我受益良多。我的网站:zhaohongcheng.com