在 HTML 內嵌 JSON object 時要注意的事情…
有時候我們會因為效能問題,在 HTML 內嵌入 JSON object,而不是再多一個 HTTP request 取得。
但「嵌入」的行為如果沒有處理好,就產生非常多 XSS attack vector 可以玩。
首先最常犯的錯誤是使用錯誤的 escape function:
<!DOCTYPE HTML> <html> <body> <script> var a = "<?= addslashes($str) ?>"; </script> </body> </html>
這樣可以用 </script><script>alert(1);// 攻擊 $str。因為 addslashes() 並不會過濾到這個字串,而產生這樣的 HTML:
<!DOCTYPE HTML> <html> <body> <script> var a = "</script><script>alert(1);//"; </script> </body> </html>
而這個字串會造成 DOM parser 解讀上產生不是我們預期的行為:

可以看到在字串裡面的 </script> 被拆開了。
這是因為瀏覽器會先拆解產生 DOM tree,再把 <script></script> 內的程式碼交給 JavaScript engine 處理。所以在一開始產生 DOM tree 的時候,是看不懂 JavaScript 程式邏輯的…
正確的方法是用 json_encode() 處理,因為 PHP 的 json_encode() 預設會把 / (slash) 變成 \/ (這是 JSON spec 裡合法的轉換):
<!DOCTYPE HTML> <html> <body> <script> var a = <?= json_encode($str) ?>; </script> </body> </html>
這會產生出:
<!DOCTYPE HTML> <html> <body> <script> var a = "<\/script><script>alert(1);//"; </script> </body> </html>
但上面這段 HTML 與 PHP code 仍然有問題,如果 $str 是 <!--<script 時,你會發現 DOM 又爛掉了:
<!DOCTYPE HTML> <html> <body> <script> var a = "<!--<script>"; </script> </body> </html>

而 escape.alf.nu 的 Level 15 就是利用這個問題,再加上其他的漏洞而完成 XSS 攻擊。
為了這個問題去 StackOverflow 上問:「 Why does <!–<script> cause a DOM tree break on the browser?」,才又發現上面這段 code 並不是合法的 HTML5 (先不管 head & title 的部份,補上後仍然不是合法的 HTML5)。
原因在於 DOM parser 對 <script></script> 的特殊處理:「 4.3.1.2 Restrictions for contents of script elements」。(話說這段 ABNF 差點讓我翻桌…)
解法是在 <script></script> 的開頭與結尾加上 HTML 註解:(這剛好是 HTML 4.01 建議的方法)
<!DOCTYPE HTML> <html> <body> <script> <!-- var a = "<!--<script>"; --> </script> </body> </html>

那段 ABNF 的目的是希望可以盡可能往後找到 --> 與 </script> 結尾的地方。
當然你也可以用 json_encode() 的 JSON_HEX_TAG 把 < 與 > 硬轉成 \u003c 與 \u003e 避開這個問題,但這使得呼叫 json_encode() 時要多一個參數 (而非預設參數),用起來比較卡…
這個問題會變得這麼討厭,是因為 DOM parser 與 JavaScript 語法之間有各自的處理方式,然後又有些 pattern 是之前的 spec 遺留下來的包袱 (像是 HTML 4.01 在「 18.3.2 Hiding script data from user agents」裡有提到用 <!-- 與 --> 包裝 <script></script>),變成在設計 HTML5 時都要考慮進去相容…
之前會習慣用 <!-- 與 //--> 包裝 <script></script> 倒不是這個原因,而是因為不這樣做的話, jQuery 在 IE 使用 html() 時遇到有 <script></script> 的字串會爛掉,所以後來寫的時候變成習慣了…
反而因為這個習慣而避開了這個問題…
超難搞啊…