16

聊一聊这个总下载量36039K的XSS-NPM库,是如何工作的?

 3 years ago
source link: http://www.cnblogs.com/zhaohongcheng/p/14224845.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Qr6Z7vM.png!mobile

上篇文章 这一次,彻底理解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()                     // 把所有‘< >’ 处理为 “< ">”

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()方法

如果说上面的正则和各种封装的方法是炮弹的话,这个FilterXSS方法就是加上火药进口的意大利炮!:boom:

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK