前端開發中務必要轉義的三處場景


  出於這樣或那樣的原因,我們在傳輸、存儲、展現字符串時需要進行轉義操作,防止不可控的事情發生。下面我將分三處場景描述,有的里邊確實有坑,希望大家看完后都有所收獲。歡迎積極留言補充。

場景1:使用URL

  前端開發中,我們經常會使用到URL,比如博客查詢的表單action:"http://eastme.me?q=前端"、Ajax發送Get\Post請求、跳轉至網址:"http://www.eastme.me/個人簡介"等等。這些請求的URL經常會出現漢字,尤其是當表單提交時,我們無法知道用戶輸入的是什么內容,傳遞的參數是否包含中文。

  標准URL只能使用英文、數字、某些部分標點符號,包含漢字的URL是無法直接使用的。瀏覽器對待此事毫不含糊,假如我們請求了上述包含漢字的地址,它會幫我們做些轉義工作。然而不幸的是,不同瀏覽器不同的操作系統不同的網頁編碼不同的請求方式下,使用的編碼方式大相徑庭。比如網址路徑用的是UTF-8的編碼,而查詢字符串使用的是操作系統的編碼。詳細請參見阮一峰:關於URL編碼

  於是,我們必須趕在瀏覽器之前對URL進行統一編碼,JavaScript為我們提供了三種編碼函數,分別是escape、encodeURI、encodeURIComponent。

  在Web2.0時代,Ajax的興起成就了越來越多的無刷新應用。我們在使用ajax提交表單,獲取數據時,包含中文的URL和參數等項務必進行提前編碼,防止兼容問題的發生。

  還有一個問題需要引起注意,當表單提交時,空格會被轉成+號。例如在搜索引擎中搜索"a b"時,搜索結果頁會給出類似"xxx.com/s?q=a+b"這樣的URL,而當真正搜索+號時,瀏覽器會將其轉為"%2B"以示區分。當使用Ajax發送Post請求時,請求實體中的參數不會被轉義,如果需要與get請求保持一致,需要使用encodeURIComponent進行顯示轉義。目前三個轉義函數中,僅encodeURIComponent函數會對"+"號進行轉義。

  所以,我們在設計API時,盡量要使用英文,數字,下划線等無須編碼就可直接使用的URL,從而防止不必要的兼容問題;我們在傳遞參數時,需要一直替瀏覽器做一下轉義操作,以保持不同情況下系統的穩定性。

場景2:表單提交或本地數據存儲

  永遠不要相信用戶輸入的內容。為了防止XSS攻擊,我們需要對用戶提交的表單進行HTML標簽轉義處理。有些場景諸如評論系統、搜索引擎自動完成,系統會將用戶輸入的內容再次吐出來顯示到頁面上。XSS攻擊的原理很簡單,某個用戶輸入腳本通過某種途徑嵌入到頁面上,這段腳本便會在任何訪問此頁面的客戶終端上再次執行。最后可能造成的影響有輕有重,不堪設想。

  舉一種最為常見的XSS攻擊的例子。本文下方有一個評論框,當有人評論時,評論的內容會跟隨文章展示出來。假如評論內容未經限制,某人輸入的評論為“<srcipt>alert('XSS')</script>”,系統在保存和展示時均未經轉義處理,將這段字符串無差異輸出到頁面上,那么任何一個訪問此篇文章的人都會看到彈出一個提示框:alert("XSS")。

  所以,當涉及到此類表單時,一定要慎重。不論是在保存時,亦或是展現時,是在前端,亦或是后端,只要在其中一環對數據進行轉義處理,就可以避免這種簡單XSS攻擊。

場景3:使用JavaScript渲染頁面

  現在,頁面上的一些功能經常會異步渲染,當用戶點擊時,才會通過Ajax異步獲取數據,然后經JavaScript解析后渲染成HTML片段,最后拼接在頁面上。假如返回的數據內容不可控,則需要盡可能的轉義以保證渲染正確。

  舉一個搜索引擎常用的auto-complete的例子。當用戶輸入內容時,客戶端發起Ajax請求,在非常短暫(10-100ms)的時間內將推薦詞返回值客戶端,通過腳本拼接成HTML片段,展示在輸入框的下方。如下圖所示:

  服務端返回的數據是這樣的:

suggest_so({"query":"a","result":[{"word":"acfun"},{"word":"apple官網"},{"word":"adidas"},{"word":"angelababy"},{"word":"acfun彈幕視頻網"},{"word":"apple"},{"word":"a直播"},{"word":"ai"},{"word":"a4紙尺寸"},{"word":"av復合視頻線"}],"version":"3.2.1","rec":"acfun"});

  這是以jsonp實現跨域的一種數據格式,暫且不管jsonp和格式如何,我們只知道返回的僅僅是字符串,展現和交互都需要前端來處理。

  有人會想,這個很簡單。拿到json字符串,轉成對象,拼接到一個<ul>標簽里,不就可以了嗎?其實就是這樣的,我們寫的腳本可能是這樣的:

var s = "<ul>";
//開始for遍歷
s += "<li data-word='" + result[i].word + "'>" + result[i].word + "</li>";
//結束遍歷
s += "</ul>";
$('#auto-c').html(s);

  但我們必須明白一點,用戶的輸入內容是不可控的,那么服務器返回的推送數據也同樣不可控。假如其中一項word包含HTML標簽或單雙引號且不經轉義,最后會拼出類似這樣的字符串插入到頁面上:

<ul>
<li data-word='don't push me'>don't push me</li>
</ul>

  上面的例子中,data-word = 'don'。

  再看類似的一個例子,現在很多搜索引擎都會做本地存儲,將用戶輸入的詞保存下來,點擊時提示:

  這對HTML的轉義要求更高。假如上面的auto-complete服務端可以為我們過濾掉"<",">"等HTML標簽,這里服務器什么也做不了。所有的數據均存儲在Local Storage中。假如不對HTML標簽轉義,最后拼接出來的字符串可能會變成這樣:

<ul>
<li data-word=""<h1>Test</h1>"">"<h1>Test</h1>"</li>
</ul>

  上面的例子中,不僅data-word變為空,內容Test也將變為h1格式。

  所以,當我們使用JavaScript渲染頁面時,假如是不可控的內容,做一次HTML標簽轉義是很有必要的。

結語

  第三個場景中,使用了jQuery的html方法將HTML片段附加在頁面上,這也是我們最為常用的一種方式。事實上改變DOM內容或節點屬性方式不止一種。想象一下,假如現在有一個變量為

var s = "<p>一個小學生 - 博客園</p>"

  DOM中有一個節點ID為demo,下面幾種方法哪個會原樣輸出,哪個又會解析成段落呢?

1.document.getElementById('demo').innerHTML = s;
2.document.getElementById('demo').setAtrribute('data-word',s);
3.var t = document.createTextNode(s);document.getElementById('demo').appendChild(t);
4.document.getElementById('demo').innerText = s; // No FireFox
5.$('#demo').html(s);
6.$('#demo').text(s);
7.$('#demo').attr('data-word',s);

  其實很簡單,只有innerHTML和jQuery的html方法把字符串解析成p標簽。這兩種情況下,假如我們就是為了顯示出"<p>",則要進行轉義,防止結果超出預期。

  PS:有人說jQuery的html方法和innerHTML一樣嘛不是,好像都是那么點功能。這只能說明你把jQuery想象的太簡單了。html方法有原生innerHTML所不能具備的很多特性,而這些特性往往正是我們需要的。比如說,假如代碼片段中有腳本,不管是外鏈還是inline,html方法會請求外鏈文件,執行所有腳本,而innerHTML則會裝作看不懂的樣子,什么都不做。DEMO:html()與innerHTML區別

  關於HTML轉義,還有一個深坑,那就是空格。我建議不要對空格進行轉義,並沒有什么用,反而會帶來意想不到的兼容性問題。

  鍵盤輸入的空格和HTML轉義的空格是兩回事,當我們對空格做了轉義時,雖然取到的數據仍然是空格,而不是&nbsp,但是其與真正的空格完全不一樣。

  所幸,"<"與"&lt;",">"與"&gt;"," " "與"&quot;" 等等不會有這樣的問題。

  請一定要記住,&nbsp !== ' '

  關於前端開發中的轉義,肯定還有很多需要注意的地方,這些東西往往不是書本中可以學來的,坑很多,踩一踩才能長記性。分享出來讓更多人避免同樣的困擾,是我寫這篇文章的初衷。歡迎你們留言補充!

  (全文完)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM