聊一聊这个总下载量3603w的xss库,是如何工作的?

上篇文章这一次,彻底理解XSS攻击讲解了XSS攻击的类型和预防方式,本篇文章我们来看这个36039K的XSS-NPM库(你没有看错就是3603W次, 36039K次,36,039,651次,数据来自https://npm-stat.com),相信挺多小伙伴在项目中,也用到了这个库。

话不多说,我们来看~

js-xss简介

js-xss是一个用于对用户输入的内容进行过滤,以避免遭受 XSS 攻击的模块(什么是 XSS 攻击?)。主要用于论坛、博客、网上商店等等一些可允许用户录入页面排版、格式控制相关的 HTML 的场景。

特性:

  • 可配置白名单控制允许的HTML标签及各标签的属性;

  • 通过自定义处理函数,可对任意标签及其属性进行处理;

js-xss有多受欢迎?

让我们来看看下面的数据:

🥇 GitHub 3.8K Star; (数据日期:2020-12-30,数据来源:js-xss-github

🥇 周下载量575,790次; (数据日期:2020-12-24 ~ 2020-12-30,数据来源:xss-npm

🥇 总下载量36,039,651次;(数据日期:2013-01-31 ~ 2020-12-30,数据来源:npm-stat.com

哪些网站在使用它?

🥇 ​Teambition

🥇 cnpmjs.org

🥇 AngularJS中文社区

🥇 CNode中文社区

🥇 前端乱炖

🥇 ​为知笔记

使用方法

在 Node.js 中使用


// 安装xss依赖

npm install xss

// 引入xss模块

const xss = require("xss");



// 使用 xss()方法处理内容

const html = xss('<script>alert("xss");</script>');

console.log(html);

CDN引入使用


// 注意请勿将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 源码阅读

下面让我们来一起看看,js-xss的库是怎么防止xss攻击的吧~

对应源码地址dist/xss.js

下面的源码分析从上到下,大家可以打开上述地址,两个窗口对比查看效果

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()                     // 把所有‘< >’ 处理为 “&lt; "&gt;”

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 = /&quot;/g;



// 匹配 大小写&#数字 全局换行忽略大小写搜索

var REGEXP_ATTR_VALUE_1 = /&#([a-zA-Z0-9]*);?/gim;



// 匹配 &colon; &newline; 

var REGEXP_ATTR_VALUE_COLON = /&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()                    // 所有的 " 替换成 &quot;

FN: unescapeQuote()                  // 所有的 &quot; 替换成 "

FN: escapeHtmlEntities()             // 处理Unicode编码  

FN: escapeDangerHtml5Entities()      // 处理&colon; &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()方法

如果说上面的正则和各种封装的方法是炮弹的话,这个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

posted @ 2021-01-04 08:46  Tz一号  阅读(314)  评论(0编辑  收藏