关于 XSS 怎样形成、如何注入、能做什么、如何防范,前人已有无数的探讨,这里不再累述了。本文介绍的则是另一种预防思路。
  几乎每篇谈论 XSS 的文章,结尾多少都会提到如何防止,然而大多万变不离其宗。要转义什么,要过滤什么,不要忘了什么之类的。尽管都是众所周知的道理,但 XSS 漏洞十几年来几乎从未中断过,不乏一些大网站也时常爆出,小网站更是家常便饭。
  预警系统
  事实上,至今仍未有一劳永逸的解决方案,要避免它依旧使用古老的土办法,逐个的过滤。然而人总有疏忽的时候,每当产品迭代更新时,难免会遗漏一些新字段,导致漏洞被引入。
  即使圣人千虑也有一失,程序出 BUG 完全可以理解,及时修复行。但令人费解的是,问题出现到被发现,却要经过相当长的时间。例如不久前贴吧 XSS 蠕虫脚本,直到大规模爆发后经用户举报,终才得知。其他网站大多也类似,直到白帽子们挖掘出漏洞,提交到安全平台上,终厂商才被告知。若遇到黑客私下留着这些漏洞慢慢利用,那只能听天由命了。
  因此,要是能有一套实时的预警系统,那更好了。即使无法阻止漏洞的发生,但能在漏洞触发的第一时间里,通知开发人员,即可在短的时间里修复,将损失降到低。各式各样的应用层防火墙,也由此产生。
  不过,和传统的系统漏洞不同,XSS 终是在用户页面中触发的。因此,我们不妨尝试使用前端的思路,进行在线防御。
  DOM 储存型 XSS
  先来假设一个有 BUG 的后台,没有很好处理用户输入的数据,导致 XSS 能被注入到页面:
  <img src="{路径}" />
  <img src="{路径" onload="alert(/xss/)}" />
  只转义尖括号,却忘了引号,是 XSS 里为常见的。攻击者们可以提前关闭属性,并添加一个极易触发的内联事件,跨站脚本这样被轻易执行了。
  那么,我们能否使用前端脚本来捕获,甚至拦截呢?
  被动扫描
  简单的办法,是把页面里所有元素都扫描一遍,检测那些 on 开头的内联属性,看看是不是存在异常:
  例如字符数非常多,正常情况下这是很少出现的,但 XSS 为了躲避转义有时会编码的很长;例如出现一些 XSS 经常使用的关键字,但在实际产品里几乎不会用到的。这些都可以作为漏洞出现的征兆,通知给开发人员。
  不过,土办法终究存在很大的局限性。在如今清一色的 AJAX 时代,页面元素从来都不是固定的。伴随着用户各种交互,新内容随时都可能动态添加进来。即使换成定期扫描一次,XSS 也可能在定时器的间隔中触发,并销毁自己,那样永远都无法跟踪到了。况且,频繁的扫描对性能影响也是巨大的。
  如同早期的安全软件一样,每隔几秒扫描一次注册表启动项,不仅费性能,而且对恶意软件几乎不起作用;但之后的主动防御系统不同了,只有在真正调用 API 时才进行分析,不通过则直接拦截,完全避免了定时器的间隔遗漏。
  因此,我们需要这种类似的延时策略 —— 仅在 XSS 即将触发时对其分析,对不符合策略的元素,进行拦截或者放行,同时发送报警到后台日志。
  主动防御
  『主动防御』,这概念放在前端脚本里似乎有些玄乎。但不难发现,这仅仅是执行优先级的事而已 —— 只要防御程序能运行在其他程序之前,我们有了可进可退的主动权。对于无比强大的 HTML5 和灵活多变的 JavaScript,这些概念都可以被玩转出来。
  继续回到刚才讨论的内联事件 XSS 上来。浏览器虽然没提供可操控内联事件的接口,但内联事件的本质仍是一个事件,无论怎样变化都离不开 DOM 事件模型。
  扯到模型上面,一切即将迎刃而解。模型是解决问题的靠谱的办法,尤其是像 DOM-3-Event 这种早已制定的模型,其稳定性毋庸置疑。
  即便没仔细阅读官方文档,但凡做过网页的都知道,有个 addEventListener 的接口,并取代了曾经一个古老的叫 attachEvent 的东西。尽管只是新增了一个参数而已,但正是这个差别成了人们津津乐道的话题。每当面试谈到事件时,总少不了考察下这个新参数的用途。尽管在日常开发中很少用到它。
  关于事件捕获和冒泡的细节,不多讨论了。下面的这段代码,或许能激发你对『主动防御』的遐想。

 

<button onclick="console.log('target')">CLICK ME</button>
<script>
document.addEventListener('click', function(e) {
console.log('bubble');
});
document.addEventListener('click', function(e) {
console.log('capture');
//e.stopImmediatePropagation();
}, true);
</script>
Run