如何通过输入过滤和输出编码防止XSS攻击

XSS攻击是Web安全的首要威胁。本文从实际问题切入,详解输入过滤与输出编码的核心原则与操作步骤,并结合Cloudflare防护能力,提供一套可落地的XSS防护方案,涵盖常见误区和失败条件。

如何通过输入过滤和输出编码防止XSS攻击
封面图:ZuCDN · ZuCDN 原创

当你的网站用户反馈说,在评论框输入一段代码后,页面突然弹出了奇怪的广告,甚至跳转到钓鱼网站,这时你很可能已经遭遇了XSS(跨站脚本)攻击。XSS攻击的本质是攻击者将恶意脚本注入到网页中,当其他用户访问时,脚本在浏览器中执行,窃取Cookie、会话令牌或篡改页面内容。防止XSS攻击最基础也最有效的两个手段就是输入过滤和输出编码。

输入过滤:第一道防线,但并非万能

输入过滤是指在数据进入应用之前,对用户提交的内容进行校验和清洗,移除或转义可能构成脚本的字符。常见的做法包括:

  • 白名单校验:只允许特定格式的输入,例如邮箱、电话号码、纯数字等。对于富文本输入,可使用白名单标签和属性,如只允许 <b><i> 等,而拒绝 <script><iframe> 等。
  • 黑名单过滤:移除或转义 <script>javascript:onerror 等危险关键词。但黑名单容易被绕过,例如大小写混写、编码变体(如 <script>)等,因此不推荐作为唯一手段。

实际操作中,输入过滤通常在服务端进行,因为客户端过滤可以被绕过。例如,在Node.js中,可以使用 validator 库进行白名单校验,或使用 xss 库进行过滤。但要注意,输入过滤并不能完全消除XSS风险,因为某些上下文(如URL、CSS)中,即使过滤了HTML标签,仍可能注入恶意代码。

输出编码:第二道防线,更可靠

输出编码是在数据输出到HTML页面时,根据上下文对特殊字符进行转义,使其被浏览器当作纯文本处理,而不是可执行的代码。与输入过滤不同,输出编码是防御XSS的关键,它确保即使输入包含恶意代码,也无法在浏览器中执行。

不同上下文需要不同的编码方式:

  • HTML实体编码:在HTML标签内容或属性值中,将 &<>"' 等转义为实体。例如,<script> 变为 &lt;script&gt;,浏览器会显示为文本。
  • JavaScript编码:在 <script> 块或事件属性中,需要对字符串进行JavaScript转义,如将 < 转义为 <
  • URL编码:在URL参数中,需要对特殊字符进行百分号编码。
  • CSS编码:在样式属性中,对特殊字符进行转义。

在开发中,应使用各语言提供的安全编码函数,例如PHP的 htmlspecialchars(),Java的 StringEscapeUtils.escapeHtml(),JavaScript的 textContent 属性等。同时,模板引擎如Vue、React默认会进行输出编码,但要注意使用 v-htmldangerouslySetInnerHTML 时会绕过编码,需谨慎使用。

结合Cloudflare防护:边缘层的补充

除了在应用层实施输入过滤和输出编码,还可以借助CDN或云防护服务在边缘层拦截XSS攻击。例如,Cloudflare的WAF(Web应用防火墙)可以检测并阻止常见的XSS攻击载荷。Cloudflare的DDoS防护也能在攻击流量到达源站前进行缓解,虽然DDoS主要针对可用性,但部分XSS攻击可能伴随大规模流量。Cloudflare的缓存和Worker服务也能提供额外的安全层:通过缓存静态内容,减少源站压力;通过Worker在边缘执行自定义逻辑,可对请求进行实时过滤或重写响应。

例如,在Cloudflare Workers中,你可以编写一个简单的中间件,对所有响应中的用户输入进行编码,或者拦截包含可疑 <script> 的请求。Worker运行在边缘节点,能够快速响应,并且无需修改源站代码。但要注意,边缘过滤并不能替代应用层的输出编码,因为攻击可能通过其他方式绕过,且Worker逻辑也需要谨慎设计。

常见误区与失败条件

在实施XSS防护时,开发者常犯以下错误:

  • 只依赖输入过滤:如上所述,输入过滤可能被绕过,且无法覆盖所有上下文。
  • 在客户端进行过滤:客户端代码可被攻击者修改,过滤无效。
  • 使用不安全的API:如使用 innerHTML 而非 textContent,或使用 eval() 执行用户输入。
  • 忽略上下文:对HTML实体编码了,但未对JavaScript上下文编码,导致在 <script> 中仍可执行。

失败条件包括:攻击者通过编码变体绕过黑名单;在富文本编辑器中,未正确使用白名单;在URL中注入 javascript: 协议;在CSS中使用 expression() 等。此外,如果应用从数据库读取数据并直接输出,即使输入时过滤了,存储型XSS仍可能发生,因此输出编码必须在每次输出时执行。

总结

防止XSS攻击,输入过滤和输出编码是相辅相成的两道防线。输入过滤应作为辅助,输出编码才是根本。开发者应遵循“编码一切输出”的原则,根据上下文选择正确的编码方式,并避免使用危险的API。同时,借助Cloudflare等云服务,可以在边缘层增加防护,但不应过度依赖。通过多层防护,才能有效降低XSS攻击的风险。

参考资料

延伸阅读