直接写一篇可能比较清楚
“存资料的时候应该存未处理过的原始资料,吐 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,你必须保留原始的输入资料