上篇文章這一次,徹底理解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
? 前端亂燉
? 為知筆記
使用方法
在 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方法就是加上火藥進口的義大利炮!?
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