直接寫一篇可能比較清楚
「存資料的時候應該存未處理過的原始資料,吐 html 的時候才做 escape」
原因有幾個
- 你先 escape 了以後,你就沒有原始資料,只剩下加料過的資料
- 東西不見得塞在 HTML 裡面,不同的地方要做不同的 escape
- 萬一未來發現了新的攻擊方法,你有機會改 code 應對,而不是進 DB 改資料
===============================================================
第一點跟第三點我就不特別討論了,這邊專注在第二點上
範例像這樣
<h1><?= $title?></h1>
<script>alert("標題是<?= $title?>")</script>
如果你存檔的時候就做 $title = htmlspecialchars($title)
那麼你就會不知道該怎麼對 javascript 做處理,於是
- <最新>高雄宇宙港遭恐怖份子攻擊
- h1 會顯示,但 script 會噴出怪訊息
- 群星齊唱"愛還記得嗎"
- h1 會顯示,但 script 會爛掉
而且完全可以寫出不會被 htmlspecialchars 影響的 xss...
正解會是這樣
<h1><?= htmlspecialchars($title)?></h1>
<script>alert("標題是" + <?= json_encode($title)?>)</script>
這些問題就算你用 template engine 也還是得注意,頂多是做起來比較輕鬆
例如 twig,預設是當成 html 來 escape,所以
<h1>{{ title }}</h1>
<script>alert("標題是{{ title }}")</script>
還是會死在 javascript,正確的做法是
<h1>{{ title }}</h1>
<script>alert("標題是{{ title|e('js') }}")</script>
BTW,這邊有個隱藏大魔王叫做 URL
雖然塞在 html 裡面,但是用 htmlspecialchars() 清理是不夠的
因為有 data: 跟 javascript: 這種東西可以用,讓 escape 變得很麻煩
這邊建議用第三方 lib 來處理(例如 htmlpurifier),或根本不讓使用者自填 url...
===============================================================
總之,你在不同的地方,需要用不同的方式 escape 資料
「事先 escape 然後到處通用」這條路...很可惜的是不通的
而為了讓每個地方都能正確的 escape,你必須保留原始的輸入資料