摘要:前言提供了函數,用于轉義字符串,替換和字符為字符實體。如果希望正確地顯示預留字符,我們必須在源代碼中使用字符實體。字符實體有兩種形式。轉義我們的應對方式就是將取得的值中的特殊字符轉為字符實體。
前言
underscore 提供了 _.escape 函數,用于轉義 HTML 字符串,替換 &, <, >, ", ", 和 ` 字符為字符實體。
_.escape("Curly, Larry & Moe"); => "Curly, Larry & Moe"
underscore 同樣提供了 _.unescape 函數,功能與 _.escape 相反:
_.unescape("Curly, Larry & Moe"); => "Curly, Larry & Moe"XSS 攻擊
可是我們為什么需要轉義 HTML 呢?
舉個例子,一個個人中心頁的地址為: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=。
就相當于:
document.getElementById("username").innerHTML = "";
會有什么效果呢?
結果是什么也沒有發生……
這是因為:
根據 W3C 規范,script 標簽中所指的腳本僅在瀏覽器第一次加載頁面時對其進行解析并執行其中的腳本代碼,所以通過 innerHTML 方法動態插入到頁面中的 script 標簽中的腳本代碼在所有瀏覽器中默認情況下均不能被執行。
千萬不要以為這樣就安全了……
你把地址改成 www.example.com/user.html?name= 呢?
就相當于:
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,發送到黑客自己的服務器,到監聽你的輸入,到發起 CSRF 攻擊,直接以你的身份調用網站的各種接口……
總之,很危險。
為了防止這種情況的發生,我們可以將網址上的值取到后,進行一個特殊處理,再賦值給 DOM 的 innerHTML。
字符實體問題是怎么進行轉義呢?而這就要談到字符實體的概念了。
在 HTML 中,某些字符是預留的。比如說在 HTML 中不能使用小于號(<)和大于號(>),因為瀏覽器會誤認為它們是標簽。
如果希望正確地顯示預留字符,我們必須在 HTML 源代碼中使用字符實體(character entities)。
字符實體有兩種形式:
&entity_name;
entity_number;。
比如說我們要顯示小于號,我們可以這樣寫:< 或 <;
值得一提的是,使用實體名而不是數字的好處是,名稱易于記憶。不過壞處是,瀏覽器也許并不支持所有實體名稱(但是對實體數字的支持卻很好)。
也許你會好奇,為什么 < 的字符實體是 < 呢?這是怎么進行計算的呢?
其實很簡單,就是取字符的 unicode 值,以 開頭接十進制數字 或者以 開頭接十六進制數字。舉個例子:
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 中,我們就可以用 喵 或者 喵 表示喵,不過“喵”并不具有實體名。
轉義我們的應對方式就是將取得的值中的特殊字符轉為字符實體。
舉個例子,當頁面地址是 www.example.com/user.html?name=123時,我們通過 getQueryString 取得 name 的值:
var name = getQueryString("name"); // 123
如果我們直接:
document.getElementById("username").innerHTML = name;
如我們所知,使用 innerHTML 會解析內容字符串,并且改變元素的 HMTL 內容,最終,從樣式上,我們會看到一個加粗的 123。
如果我們轉義,將 123 中的 < 和 > 轉為實體字符,即 123,我們再設置 innerHTML,瀏覽器就不會將其解釋為標簽,而是一段字符,最終會直接顯示 123,這樣就避免了潛在的危險。
思考那么問題來了,我們具體要轉義哪些字符呢?
想想我們之所以要轉義 < 和 > ,是因為瀏覽器會將其認為是一個標簽的開始或結束,所以要轉義的字符一定是瀏覽器會特殊對待的字符,那還有什么字符會被特殊對待的呢?(O_o)??
& 是一個,因為瀏覽器會認為 & 是一個字符實體的開始,如果你輸入了 <,瀏覽器會將其解釋為 <,但是當 < 是作為用戶輸入的值時,應該僅僅是顯示用戶輸入的值,而不是將其解釋為一個 <。
" 和 " 也要注意,舉個例子:
服務器端渲染的代碼為:
function render (input) { return "" }
input 的值如果直接來自于用戶的輸入,用戶可以輸入 "> ,最終渲染的 HTML 代碼就變成了:
">
結果又是一次 XSS 攻擊……
最后還有一個是反引號 `,在 IE 低版本中(≤ 8),反引號可以用于關閉標簽:
所以我們最終確定的要轉義的字符為:&, <, >, ", ", 和 `。轉義對應的值為:
& --> & < --> < > --> > " --> " " --> ' ` --> <
值得注意的是:單引號和反引號使用是實體數字、而其他使用的是實體名稱,這主要是從兼容性的角度考慮的,有的瀏覽器并不能很好的支持單引號和反引號的實體名稱。
_.escape那么具體我們該如何實現轉義呢?我們直接看一個簡單的實現:
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 系列
underscore 系列目錄地址:https://github.com/mqyqingfeng/Blog。
underscore 系列預計寫八篇左右,重點介紹 underscore 中的代碼架構、鏈式調用、內部函數、模板引擎等內容,旨在幫助大家閱讀源碼,以及寫出自己的 undercore。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93813.html
摘要:創建一個全局對象在瀏覽器中表示為對象在中表示對象保存下劃線變量被覆蓋之前的值如果出現命名沖突或考慮到規范可通過方法恢復被占用之前的值并返回對象以便重新命名創建一個空的對象常量便于內部共享使用將內置對象的原型鏈緩存在局部變量方便快速調用將 // Underscore.js 1.3.3 // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc....
摘要:所以它與其他系列的文章并不沖突,完全可以在閱讀完這個系列后,再跟著其他系列的文章接著學習。如何閱讀我在寫系列的時候,被問的最多的問題就是該怎么閱讀源碼我想簡單聊一下自己的思路。感謝大家的閱讀和支持,我是冴羽,下個系列再見啦 前言 別名:《underscore 系列 8 篇正式完結!》 介紹 underscore 系列是我寫的第三個系列,前兩個系列分別是 JavaScript 深入系列、...
摘要:前言本篇接著上篇系列之實現一個模板引擎上。字符串中的每個字符均可由一個轉義序列表示。在中,有四個字符被認為是行終結符,其他的折行字符都會被視為空白。 前言 本篇接著上篇 underscore 系列之實現一個模板引擎(上)。 鑒于本篇涉及的知識點太多,我們先來介紹下會用到的知識點。 反斜杠的作用 var txt = We are the so-called Vikings from th...
摘要:值得注意的是,如果值在前面也就是值小于值,那么值域會被認為是零長度,而不是負增長。 underscore.js源碼加注釋一共1500多行,它提供了一整套函數式編程實用的功能,一共一百多個函數,幾乎每一個函數都可以作為參考典范。初讀的時候,真是一臉懵圈,各種函數閉包、迭代和嵌套的使用,讓我一時很難消化。在這里,我來記錄一下我學習underscore.js的一些發現,以及幾個我認為比較經典...
摘要:第一版我們來嘗試實現第一版第一版為了驗證是否有用文件文件完整的可以查看示例一在這里我們使用了,實際上在文章中使用的是構造函數。構造函數創建一個新的對象。 前言 underscore 提供了模板引擎的功能,舉個例子: var tpl = hello: ; var compiled = _.template(tpl); compiled({name: Kevin}); // hello:...
閱讀 2582·2021-11-25 09:43
閱讀 1855·2021-09-22 15:26
閱讀 3723·2019-08-30 15:56
閱讀 1710·2019-08-30 15:55
閱讀 1892·2019-08-30 15:54
閱讀 810·2019-08-30 15:52
閱讀 3154·2019-08-29 16:23
閱讀 891·2019-08-29 12:43