有時候我們會因為效能問題,在 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>
的字串會爛掉,所以後來寫的時候變成習慣了...
反而因為這個習慣而避開了這個問題...
超難搞啊...