上篇文章這一次,完全理解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