XSS攻击绕过CSP(内容安全策略)最常见的手段之一就是字符编码绕过。核心原理很简单:CSP通常基于明文字符串匹配来拦截恶意脚本,但浏览器在解析HTML时会对各种编码形式进行解码还原,攻击者利用这一"解码差"将恶意代码编码后注入,从而骗过CSP的检测规则。比如把<script>写成<script>或者用十六进制\x3cscript\x3e的形式,CSP规则如果只匹配了原始标签字符串,就会放行这段"看起来无害"的编码内容,而浏览器最终渲染时又会还原成可执行的脚本。这就是字符编码绕过的本质——利用安全策略和浏览器解析引擎之间的认知差异。
CSP到底是什么,为什么还会被绕过
CSP全称Content Security Policy,是一种HTTP响应头机制,用来限制网页中哪些资源可以被加载和执行。它的核心思路是"白名单"——你只允许来自特定域名的脚本运行,其他来源的脚本一律拦截。正常情况下,CSP确实能有效阻止大部分XSS攻击。但问题在于,CSP的匹配逻辑是基于原始HTTP响应内容的字符串比对,它不会像浏览器那样去做完整的HTML解码。这就给攻击者留下了可乘之机。当恶意代码经过编码处理后,CSP规则可能识别不出这是一段脚本,从而放行。而浏览器在渲染页面时,会自动完成解码过程,最终执行恶意代码。这种"策略层"和"渲染层"之间的不同步,就是所有编码绕过技术的根基。
常见的字符编码绕过方式详解
第一种是HTML实体编码绕过。这是最基础也最容易被忽视的方式。攻击者把尖括号和关键字转成HTML实体,比如:
<script>alert(1)</script>
这里<代表"<",>代表">"。如果CSP规则写的是检测script关键字,而服务器端的过滤或CSP配置没有考虑到实体编码后的还原,那么这段代码就能顺利通过。浏览器解析HTML时会自动把实体还原成原始字符,脚本照样执行。
第二种是Unicode编码绕过。JavaScript支持用\uXXXX的形式表示Unicode字符。攻击者可以这样写:
\u003cscript\u003ealert(1)\u003c/script\u003e
\u003c就是"<"的Unicode表示,\u003e就是">"。这种方式在JavaScript字符串内部尤其有效,因为JS引擎会先解析Unicode转义再执行。如果CSP规则没有对这种形式做覆盖,攻击就能成功。
第三种是十六进制编码绕过。在某些上下文中,比如CSS的expression()或者URL编码场景中,可以用十六进制表示字符:
\x3cscript\x3ealert(1)\x3c/script\x3e
这种方式在旧版浏览器或者某些特殊解析环境中尤其危险。虽然现代浏览器对CSS中的expression()已经限制很多,但在特定的CSP配置缺陷下仍然可能生效。
第四种是双重编码或多重编码。攻击者把已经编码过的内容再编码一次,比如先做URL编码再做HTML实体编码:
%26%2360%3Bscript%26%2362%3B
这种多层嵌套的编码方式会让检测逻辑变得极其复杂。如果服务端只做了一层解码就去匹配CSP规则,那多出来的编码层就成了天然的伪装。有些WAF(Web应用防火墙)就是栽在这种多重编码上的。
第五种是利用不常见的编码形式,比如UTF-7编码。UTF-7是一种早已被淘汰的编码方式,但某些邮件客户端和老旧系统仍然支持。攻击者可以用UTF-7编码来表示脚本标签:
+ADw-script+AD4-alert(1)+ADw-/script+AD4-
这里+ADw-代表"<",+AD4-代表">"。由于UTF-7在现代Web开发中几乎不被使用,很多安全产品根本不会去检测这种编码,绕过成功率反而很高。
CSP配置缺陷是绕过成功的关键前提
必须强调一点:字符编码绕过能成功,前提是CSP本身配置就有问题。如果CSP规则写得足够严谨,比如使用了nonce(随机数)或者hash(哈希值)机制,那么编码绕过基本无效。因为nonce机制要求每个内联脚本都必须带有服务器生成的随机令牌,攻击者无法预测也无法伪造;hash机制则要求脚本内容的SHA哈希值必须在白名单中,任何编码变化都会导致哈希值改变,从而被拦截。
但现实中,大量网站的CSP配置只用了最简单的script-src 'self'这种域名白名单方式,甚至有些网站根本没有正确设置CSP头。在这种情况下,编码绕过就成了非常实用的攻击手段。根据行业安全报告统计,超过60%的网站CSP配置存在可被利用的缺陷,其中最常见的就是缺少对编码变体的覆盖。
实际案例:某电商平台的CSP绕过过程
在一次安全测试中,某电商平台的搜索功能存在反射型XSS漏洞。该平台设置了CSP头,规则大致如下:
Content-Security-Policy: script-src 'self' https://cdn.example.com;
测试人员首先尝试直接注入<script>alert(1)</script>,被CSP拦截。然后尝试用HTML实体编码<script>alert(1)</script>,发现竟然通过了CSP检测,浏览器正常渲染并执行了脚本。原因是该平台的CSP检测逻辑只对原始响应内容做了关键字匹配,没有先对内容进行HTML实体解码再匹配。这就是一个典型的字符编码绕过CSP的真实场景。
进一步测试发现,该平台还存在另一个问题:它的CSP规则允许unsafe-inline(虽然没有显式写,但由于某些第三方组件的需要,实际上等效于允许了内联脚本)。这意味着即使没有编码绕过,攻击者也可以通过其他方式注入内联脚本。编码绕过只是在这个已经宽松的策略上又加了一层"保险"。
如何防御字符编码绕过CSP的攻击
第一,使用nonce或hash机制替代简单的域名白名单。这是最根本的解决方案。每次页面加载时服务器生成一个随机nonce值,内联脚本必须携带这个值才能执行。攻击者无法提前知道nonce,编码绕过自然失效。具体配置示例:
Content-Security-Policy: script-src 'nonce-rAnd0mStr1ngHere' 'self';
第二,在服务端对所有用户输入进行严格的解码和规范化处理后再进行安全检测。也就是说,不要只检测原始输入,要先把所有可能的编码形式都还原成明文,然后再做匹配。比如先做HTML实体解码、URL解码、Unicode解码,统一转成纯文本后再检查是否包含危险关键字。
第三,部署具备深度解码能力的WAF。传统WAF往往只做一层解码,容易被多重编码绕过。现代WAF应该具备递归解码能力,能够把多层编码全部剥开后再做检测。同时要定期更新规则库,把UTF-7等冷门编码也纳入检测范围。
第四,实施输出编码。在将用户数据输出到HTML页面时,始终进行上下文相关的编码。比如在HTML正文中输出要做HTML实体编码,在JavaScript字符串中输出要做JS转义,在URL参数中输出要做URL编码。这样即使攻击者注入了恶意代码,输出时也会被编码成无害形式。
第五,定期进行安全审计和渗透测试。CSP配置不是一劳永逸的,随着业务发展和第三方组件引入,CSP规则可能会变得越来越宽松。建议每季度做一次全面的安全评估,重点检查CSP头的完整性和有效性。
行业趋势与深度思考
从行业发展来看,CSP绕过技术一直在和防御技术"军备竞赛"。早期的CSP几乎只防得住最简单的脚本注入,后来出现了nonce和hash,绕过难度大幅提升。但攻击者也在不断寻找新的突破口,比如利用CSP的report-uri机制本身来探测nonce值(通过错误报告获取信息),或者利用strict-dynamic的配置漏洞。字符编码绕过虽然是"老技术",但在CSP配置不当的网站上依然非常有效,这说明安全的短板往往不在技术本身,而在实施和运维。
作为安全从业者,我的建议是:不要把CSP当成银弹。它是纵深防御体系中的一环,必须配合输入验证、输出编码、WAF、安全编码规范等多层措施才能真正发挥作用。单一依赖CSP而忽视其他环节,就像只装了一道门锁却把窗户全打开一样。字符编码绕过案例反复提醒我们,安全策略的有效性永远取决于最薄弱的那个环节。
总结来说,XSS绕过CSP的字符编码绕过本质上是利用了安全检测和浏览器解析之间的"信息差"。防御的核心在于消除这个信息差——让安全检测看到的内容和浏览器最终解析的内容完全一致。做到这一点,编码绕过就无从下手。
