Web应用防火墙的日志里,XSS和CSRF依然是出现频率最高的两类中危以上漏洞。很多团队把精力放在WAF绕过和复杂攻击链上,却忽略了这两种漏洞的根源从未改变:对用户输入的信任和对浏览器行为的失控。直接切入正题,我们不讲基础概念,直接看当下最有效的防御落地方案。
输出侧的终极防线:上下文感知的编码策略XSS防御的核心不在输入过滤,而在输出编码。输入过滤只能作为纵深防御的第一层,绝不能作为主要手段。因为攻击者可以通过各种编码、混淆、拼接来绕过输入端的黑名单,但无论输入怎么变形,只要在输出时根据上下文做了正确的编码处理,恶意代码就无法执行。真正有效的做法是根据数据被插入的HTML上下文,选择完全不同的编码函数。把数据放进HTML标签之间时,必须做HTML实体编码,将<转成<,将>转成>,将"转成",将'转成',将&转成&。把数据放进HTML属性里时,除了实体编码,还必须用引号把属性值包裹起来。把数据放进JavaScript代码块里时,HTML实体编码完全无效,必须做JavaScript编码,将所有非字母数字字符转成\xHH或\uHHHH格式。把数据放进URL参数里时,必须做URL编码。把数据放进CSS里时,必须做CSS编码。这五种上下文对应的编码函数完全不同,用错一个就等于没做防护。
现代前端框架如React和Vue已经内置了JSX和模板的自动转义机制,这大幅降低了XSS风险。但危险点在于那些绕过框架直接操作DOM的代码。任何时候使用innerHTML、document.write、eval、setTimeout(string)、setInterval(string)这类API,都必须先对数据做对应上下文的编码处理。如果业务必须渲染富文本内容,唯一安全的方式是使用DOMPurify这类经过严格审计的净化库做白名单过滤,只允许特定的安全标签和属性通过,其余全部剔除。自行编写正则表达式过滤富文本几乎一定会产生遗漏,这条路已经被无数次绕过验证过。
内容安全策略的精细化部署CSP是防御XSS的第二道防线,它的设计思路是限制浏览器只执行来自可信源的脚本,即使攻击者成功注入了恶意代码,浏览器也会拒绝执行。CSP的配置需要精细到每个具体资源的加载策略。script-src指令应该严格限制为'self'加上具体的CDN域名,绝对不要使用'unsafe-inline'和'unsafe-eval',这两个指令会让CSP形同虚设。如果业务中确实存在内联脚本无法完全移除的情况,必须使用nonce或hash机制。nonce是服务端每次响应时随机生成的一个加密字符串,同时出现在CSP头和合法脚本的nonce属性中,攻击者无法预测这个值,所以注入的脚本没有正确的nonce就不会被执行。hash机制是计算每个合法内联脚本的SHA-256哈希值,把哈希值加入CSP头的script-src中,浏览器会对比脚本内容的哈希值,匹配才允许执行。
部署CSP不能一蹴而就,建议先用Content-Security-Policy-Report-Only头来收集违规报告,分析哪些资源被误拦,逐步调整策略,确认无误后再切换到强制执行的Content-Security-Policy头。report-uri或report-to指令必须配置,这样一旦发生XSS攻击被CSP拦截,安全团队能第一时间收到告警。style-src、img-src、font-src、connect-src、frame-src等指令也应该逐一收紧,默认只允许'self',按需添加合法域名。object-src应该设置为'none',因为Flash和Java Applet已经退出历史舞台,这个指令留着只有风险没有收益。base-uri指令设置为'self',防止攻击者注入base标签劫持相对路径的资源加载。
CSRF防御的三层架构CSRF攻击的本质是攻击者诱导用户在已登录的站点上执行非本意的操作,浏览器会自动携带目标站点的Cookie,服务端无法区分这个请求是用户主动发起还是被伪造的。防御CSRF最成熟的方案是同步器令牌模式,也就是我们常说的CSRF Token。服务端生成一个随机、不可预测的Token,存储在用户会话中,同时把这个Token嵌入到前端页面的表单隐藏字段或请求头中。每次状态变更请求提交时,服务端校验请求中的Token与会话中的Token是否一致。Token必须满足几个硬性要求:使用密码学安全的随机数生成器生成,长度至少128位;与用户会话强绑定,不能全局共用;每次使用后立即失效并重新生成,或者采用每个会话有效期内不变但严格绑定会话的策略;Token绝对不能出现在URL参数中,只能通过POST请求体或自定义请求头传输,因为URL会被浏览器历史、Referer头、日志系统记录而泄露。
对于前后端分离的SPA应用,CSRF Token的传递方式需要调整。可以在前端登录成功后,由服务端在响应头中设置一个HttpOnly为false的Cookie来存储Token,前端JavaScript读取这个Cookie的值,然后在每次请求的X-CSRF-Token请求头中携带。服务端校验时对比请求头中的Token和Cookie中的Token是否一致。攻击者无法读取跨域的Cookie内容,也无法设置自定义请求头,所以这个方案是安全的。但要注意Cookie的SameSite属性必须设置为Strict或Lax,这是CSRF防御的第二层。SameSite=Strict表示完全禁止跨站发送Cookie,SameSite=Lax允许部分安全的跨站请求携带Cookie,比如从外部链接跳转过来的GET请求。将SameSite设置为Lax可以兼顾安全性和用户体验,绝大多数场景下已经足够。第三层是验证Origin和Referer请求头。服务端检查Origin头是否与预期的域名完全匹配,如果Origin不存在,则检查Referer头。这两个头可能被某些浏览器设置或代理工具移除,所以不能单独依赖,但作为辅助检查层很有价值,能拦截那些没有正确携带Token但跨域发起的请求。
双重提交Cookie模式的适用场景与局限在某些无法在服务端维护会话状态的架构中,比如无状态API或CDN缓存的静态页面,传统的CSRF Token方案难以实施。这时可以使用双重提交Cookie模式。服务端在用户登录后设置一个随机值的Cookie,前端JavaScript读取这个Cookie的值,并将其作为请求头X-CSRF-Token发送。服务端只需要对比Cookie中的值和请求头中的值是否一致。攻击者无法读取跨域Cookie,也无法设置自定义请求头,所以无法构造有效请求。但这个模式的安全性依赖于Cookie的SameSite属性和子域名安全。如果存在子域名劫持漏洞,攻击者可以在子域名下设置Cookie,从而绕过校验。因此这个方案只能用在确认所有子域名都安全可控的场景下,且必须配合SameSite=Strict使用。
登录态与敏感操作的额外加固CSRF防御通常聚焦在状态变更请求上,但登录操作本身也需要防CSRF。攻击者可以构造一个CSRF请求,让受害者浏览器以攻击者的账号登录,如果受害者在不知情的情况下使用了这个账号,其浏览记录、输入内容都可能被攻击者查看。登录接口同样需要CSRF Token保护,或者使用双重提交Cookie模式。对于修改密码、修改绑定手机或邮箱、转账支付这类高危操作,即使有了CSRF Token,也应该叠加二次身份验证,要求用户输入当前密码或提供短信验证码。这样即使CSRF Token因某种原因被绕过,攻击者也无法完成关键操作。
XSS与CSRF的交叉防御XSS和CSRF的防御不是孤立的。一旦站点存在XSS漏洞,所有基于Token的CSRF防御都会失效,因为攻击者可以通过XSS注入脚本读取页面中的Token,读取请求头,甚至直接发起同源请求。所以XSS防御是CSRF防御的前提条件。反过来,如果CSRF防御存在漏洞,攻击者可以结合登录CSRF和XSS漏洞实施更复杂的攻击链。安全团队必须把这两个漏洞的防御作为一个整体来设计和验证。在代码审查和安全测试中,发现XSS漏洞时,必须同步评估该页面是否存在CSRF敏感的接口,判断攻击者能利用XSS达到什么程度的破坏。发现CSRF漏洞时,也要追溯是否存在XSS漏洞放大了攻击面。
实际部署中的常见误区和检查清单很多团队在防御方案落地时,会陷入一些典型的误区。第一个误区是只在前端做校验,服务端没有独立校验。前端校验是用户体验层面的,服务端校验才是安全层面的,两者缺一不可,但服务端校验绝对不能省略。第二个误区是CSRF Token使用弱随机数算法,比如用时间戳加用户名做MD5,这种Token可以被攻击者预测。必须使用SecureRandom这类密码学安全的随机数生成器。第三个误区是CSP配置过于宽松,比如为了方便使用了'unsafe-inline',等于没有部署CSP。第四个误区是认为HTTPS站点不需要CSRF防御,HTTPS只保护传输层,不防御CSRF攻击。第五个误区是只对POST请求做CSRF校验,忽略了PUT、DELETE、PATCH等HTTP方法同样可以用于状态变更,这些方法也必须纳入CSRF防御范围。
一套完整的验证清单应该包括:所有用户输入在输出时是否根据上下文做了正确的编码处理;是否禁用了所有不安全的DOM操作API;CSP头是否配置且没有unsafe-inline和unsafe-eval;CSRF Token是否在所有状态变更请求中校验;Token是否使用安全随机数生成且绑定会话;SameSite Cookie属性是否设置;Origin或Referer头是否作为辅助校验;登录和敏感操作是否有额外身份验证;所有子域名是否安全可控;是否建立了CSP违规监控和CSRF异常告警机制。逐项检查落实后,这两类漏洞的残余风险才能降到可接受的范围内。
