跨站脚本XSS攻击原理,网站该如何做好防护
脚本是怎么跑到别人浏览器里的
问题的起点是一个很朴素的认知:浏览器分不清页面里哪段内容是你写的,哪段是别的用户提交的。只要一段文本被当成 HTML 渲染,里面的脚本标签就会被执行,而且是在当前站点的域下执行,能读到这个站点的 Cookie、能代替用户发起请求。攻击者要做的,就是想办法让一段自己控制的文本出现在别人看到的页面里。
三种不同的形态
反射型最容易理解:恶意内容放在链接参数里,用户点开之后,参数被页面直接输出到 HTML 里,脚本随之执行。它需要诱导用户点击,因此多出现在评论、搜索、跳转这类入口。存储型要隐蔽得多,内容提交时看起来只是普通文本,入库也是干净的,直到某个页面把它渲染出来才变成脚本;因为不需要用户点特定链接,只要有页面展示这段内容,访问者就会中招,影响面往往更大。还有一种发生在浏览器内部,页面用脚本处理地址栏参数,把不可信的内容拼进了页面,服务端从头到尾没参与,日志里看不到任何异常。

防护的核心在输出
很多人把注意力放在输入过滤上,比如拦掉尖括号和引号,这在实际业务里很难做对,因为同样的字符在有些场景里就是正常内容,一刀切会误伤。更可靠的做法是在输出的时候按上下文做编码:放进 HTML 正文的,转义尖括号和引号;放进元素属性的,额外注意引号;放进脚本块或者地址里的,要用对应的编码方式。一句话概括,内容进入具体位置之前,先按那个位置的规则处理。

再加两层兜底
即便输出处理有疏漏,还有两个手段可以减小损失。一是内容安全策略,通过响应头告诉浏览器只允许从哪些来源加载脚本,把内联脚本和陌生域名挡掉,这样即使注入成功,脚本也执行不了。二是给会话 Cookie 加上禁止脚本读取的属性,让窃取登录凭证这条路走不通。两者都不需要改动页面逻辑,成本很低。
怎么验证有没有处理干净
测试方法比想象中简单:在会展示用户输入的每个位置提交一段带标签的文本,看页面上是原样显示还是被当成标签渲染。常见的漏点包括搜索结果页的关键词回显、个人资料里的昵称和签名、后台的审计列表,以及邮件模板。逐一试一遍,比读代码更快发现问题。
修补完成之后还要回头看一眼历史数据。过去已经入库的内容里可能已经带着脚本,输出环节修好了固然能挡住渲染,但这些数据在别的地方被使用时仍有风险,需要一并清理。
容易漏掉的两个位置
说起脚本注入,多数人先想到输入框和查询参数,其实请求头同样会被带进页面。来源地址、浏览器标识这些字段,如果被站点直接显示在后台页面里,同样可能成为注入点。抓包工具能改,脚本也能改,这类输入并不比表单更安全。
另一个位置是富文本。允许编辑带格式内容的功能,风险比普通输入高得多,因为里面本来就允许出现标签。防护不能只靠过滤几个关键词,而是先把允许的标签和属性列成明确清单,其余一律不保留,再对链接协议单独校验。这样即便规则有疏漏,能造成的影响也被限制在很小的范围内。
改完之后,用几组过去能生效的样例再试一次,确认确实拦住了。把这些样例留下来,后面每次改动都能复用。
同一个校验写在哪里,效果差别很大。只在前端做,改一改请求就能绕过去;只在后端做,体验会差一些但安全得多。常见的做法是两边都做:前端负责及时提示,后端负责最终判断,两边规则保持一致。
模板渲染也要留意。不同的模板引擎对转义的默认行为不一样,有的默认转义,有的需要显式声明。接手别人的代码时,先确认这一点,再谈其他加固。
浏览器提供的内容安全策略值得用起来,它是一层兜底:即使某处渲染漏了转义,页面里不该执行的脚本也很难跑起来。
顺序上先改渲染,再叠加兜底,效果最稳。