跳到主要内容

XSS 与 CSRF

面试速答

  • XSS(Cross-Site Scripting,跨站脚本):攻击者把恶意脚本注入到你的页面里,让浏览器替攻击者执行。核心问题是“不可信内容被当成代码执行了”。
  • CSRF(Cross-Site Request Forgery,跨站请求伪造):攻击者诱导用户在已登录状态下访问第三方页面,借浏览器自动携带凭证的能力,向目标站点发起伪造请求。核心问题是“请求是浏览器发的,但不是用户真意愿”。
  • 本质区别
    • XSS 解决的是“页面里是否能执行恶意脚本”。
    • CSRF 解决的是“跨站请求是否能冒用用户身份”。
  • 防御重点
    • 防 XSS:输出编码、避免危险 DOM API、内容消毒、CSP、敏感 Cookie 加 HttpOnly
    • 防 CSRF:SameSite、CSRF Token、校验 Origin/Referer、关键操作二次确认。
  • 常见追问HttpOnly 能防 XSS 窃 Cookie,但不能阻止 XSS 直接在当前站内发请求;所以 XSS 一旦成立,很多 CSRF 防线也会被绕过。

心智模型

可以把这两个攻击理解成两条完全不同的入侵路径:

一句话记忆:

  • XSS 是“脚本进来了”
  • CSRF 是“请求被冒用了”

一张表讲清差异

维度XSSCSRF
攻击目标让站点执行攻击者脚本让站点接收攻击者伪造请求
前提条件页面存在注入点用户已登录且浏览器会自动带凭证
利用媒介HTML、JS、DOM、模板渲染Cookie、自动登录态、跨站请求
是否需要读响应往往不需要,能执行 JS 就够危险通常也不需要读响应,只要请求成功
典型影响窃取信息、劫持页面、伪造操作冒用身份发起敏感操作
核心防线编码、消毒、CSP、HttpOnlySameSite、CSRF Token、来源校验

XSS:为什么会发生

1. 根因

当你把“不可信字符串”放进一个会被浏览器当成 HTML 或 JS 解释的位置,就可能触发 XSS。

常见高危位置:

  • innerHTML
  • outerHTML
  • insertAdjacentHTML
  • 动态拼接事件属性,如 onclick
  • 把用户输入直接塞进模板后再渲染
  • 服务端返回富文本但前端不做白名单消毒

2. 典型示例

const comment = new URLSearchParams(location.search).get('comment') || '';

// ❌ 危险:把用户输入当 HTML 插进去
document.querySelector('#app')!.innerHTML = `
<div class="comment">${comment}</div>
`;

如果 comment 是下面这类内容,就可能执行恶意逻辑:

<img src=x onerror="fetch('/api/profile',{credentials:'include'})">

3. 更安全的写法

const comment = new URLSearchParams(location.search).get('comment') || '';
const node = document.querySelector('#app')!;

// ✅ 当成纯文本插入,不给浏览器解释成 HTML 的机会
node.textContent = comment;

如果业务必须渲染富文本,就不能只说“后端保证安全”,而是要做白名单消毒

import DOMPurify from 'dompurify';

const html = DOMPurify.sanitize(serverHTML);
document.querySelector('#preview')!.innerHTML = html;

XSS 的分类

1. 反射型 XSS

恶意输入跟着本次请求进、本次响应出,通常藏在 URL 参数、搜索词、错误提示里。

2. 存储型 XSS

恶意内容先被保存到数据库,再被其他用户打开页面时触发。危害更大,因为它是“一次注入,多人中招”。

3. DOM 型 XSS

问题不一定出在服务端模板,而是前端脚本自己从 locationdocument.cookiepostMessage 等位置取值,再危险拼接到 DOM。


XSS 的攻击链

这里要强调一个常被问到的点:

  • XSS 不只是偷 Cookie
  • 只要脚本跑在当前站点上下文里,它就能:
    • 读取页面里的敏感信息
    • 伪造点击、改 DOM、挂钩表单
    • 直接调用当前站点接口
    • 操作本地存储里的 token

所以“我们把 Cookie 设成了 HttpOnly,因此不怕 XSS”是错误说法。准确表达应是:

  • HttpOnly 只能降低“Cookie 被 JS 直接读取”的风险
  • 不能消灭 XSS 本身

CSRF:为什么会发生

1. 根因

浏览器会自动为目标站点带上 Cookie。攻击者不需要拿到 Cookie 的值,只要让用户浏览器向目标站点发请求,就可能冒用用户身份。

2. 典型场景

假设用户已经登录 bank.example.com,此时访问了恶意站点:

<form action="https://bank.example.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="5000" />
</form>
<script>
document.forms[0].submit();
</script>

如果目标站点只依赖 Cookie 判断登录态,又没有额外的跨站保护,那么浏览器可能会自动携带 Cookie,把这笔请求当成用户本人提交。

3. 攻击成立条件

  • 用户处于登录状态
  • 凭证会被自动带上,最典型的是 Cookie
  • 服务端只校验“有没有登录”,不校验“是不是用户主动发起”
  • 接口允许跨站方式命中,或缺少额外防线

CSRF 的请求路径


如果前端把访问凭证放在 JS 主动控制的请求头里,例如:

Authorization: Bearer <access_token>

那浏览器不会像 Cookie 一样自动带上它。这意味着攻击者仅靠一个跨站表单或图片请求,通常无法伪造出带 Authorization 头的请求。

但注意边界:

  • 这只能说明“天然更抗 CSRF
  • 不代表更安全
  • 如果站点有 XSS,攻击脚本照样可以在页面上下文里读到 localStorage 或内存里的 token

所以实际工程里经常是:

  • Cookie 方案重点补 CSRF 防线
  • Token 方案重点补 XSS 防线

如何防 XSS

1. 输出编码优先,不要事后赌补救

原则是:

  • 插到 HTML 文本节点,就按文本处理
  • 插到属性里,就做属性级转义
  • 插到 URL 里,就做 URL 级校验
  • 能不用 HTML 解析,就不要用

2. 少碰危险 DOM API

优先级建议:

  • 优先 textContent
  • 其次是安全模板渲染
  • 最后才是经过消毒后的 innerHTML

3. 对富文本做白名单消毒

关键点不是“去掉几个危险标签”,而是:

  • 允许哪些标签
  • 允许哪些属性
  • 是否允许内联样式
  • 是否允许 href="javascript:..."

4. 配置 CSP(Content Security Policy)

它不能替代编码和消毒,但能显著抬高利用门槛。一个常见思路是:

Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-<random>';
object-src 'none';
base-uri 'self';

它的价值在于:

  • 限制脚本来源
  • 尽量禁用内联脚本
  • 降低注入后立即执行的概率

这样即使出现 XSS,攻击脚本也不能直接通过 document.cookie 读出认证 Cookie。

6. 减少前端可接触的高价值数据

例如:

  • 不把长期有效密钥放在 localStorage
  • 页面只保留当前渲染必须的数据
  • 退出登录时清理内存和缓存

如何防 CSRF

1. SameSite

现代浏览器里,SameSite 已经是第一层基础防线。

  • Strict:最严,几乎所有跨站场景都不带 Cookie
  • Lax:兼顾体验,能拦住大多数跨站子请求和跨站 POST
  • None:允许跨站带 Cookie,但必须配合 Secure

一句话建议:

  • 普通业务优先 Lax
  • 高敏后台优先 Strict
  • 真有跨站嵌入需求才考虑 None; Secure

2. CSRF Token

核心思想是:登录态凭证是浏览器自动带的,但 CSRF Token 必须由当前业务页面主动提交。攻击者站点通常拿不到这个值。

常见做法:

  • 页面初始化时由服务端下发一个随机 token
  • 前端把它放进请求头或表单隐藏字段
  • 服务端校验 token 是否匹配当前会话

示例:

await fetch('/api/profile', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify({nickname: 'terry'}),
});

3. 校验 Origin / Referer

这属于服务端辅助防线,适合拦截明显的跨站来源。面试里可以这样说:

  • 能校验 Origin 优先校验 Origin
  • 不存在时再兜底看 Referer
  • 但不要把它当成唯一手段

4. 敏感操作二次确认

例如修改密码、换绑手机、支付转账:

  • 再输密码
  • 短信/邮箱验证码
  • 一次性确认码

这不是直接“防 CSRF”的协议手段,但能显著降低被静默伪造后的损失。


XSS 和 CSRF 的关系

最容易被问的关系题是:

1. XSS 能不能绕过 CSRF 防御

大多数时候可以。

原因很简单:如果恶意脚本已经跑在你的站点里,它就能像正常前端代码一样:

  • 读取页面里的 CSRF Token
  • 发起同源请求
  • 操作表单和按钮

所以先挡住 XSS,再谈 CSRF,是很重要的安全优先级判断。

2. CSRF 会不会导致 XSS

通常不会。它们不是因果关系,而是两个不同方向的攻击面。

3. SameSite 能防 XSS 吗

不能。SameSite 只控制 Cookie 的跨站发送行为,不解决页面里脚本被执行的问题。


面试高频问答

1. XSS 和 CSRF 最大区别是什么?

:XSS 是把恶意脚本注入到站点里执行,利用的是页面对不可信内容的错误处理;CSRF 是借浏览器自动携带登录凭证的行为,伪造用户请求。前者重点防“代码执行”,后者重点防“身份冒用”。

2. HttpOnly 为什么不能完全防住 XSS?

:它只能阻止 JS 读取 Cookie,不能阻止恶意脚本在页面里执行。攻击脚本依然可以读 DOM、发同源请求、篡改页面、冒充用户操作。

3. 为什么 Authorization 头方案通常不容易被 CSRF?

:因为浏览器不会像 Cookie 那样自动为跨站表单或图片请求带上 Authorization 头。攻击者很难在跨站页面里构造出带该请求头的合法请求。但如果有 XSS,token 仍可能被窃取或滥用。

4. 只配 SameSite=Lax 就够了吗?

:不够。它能拦住大量常见跨站请求,但高价值操作仍建议配合 CSRF Token、来源校验和二次确认。安全设计最好是多层防线,而不是单点依赖。

5. DOMPurify 这种库是不是用了就绝对安全?

:不是。它能降低富文本渲染风险,但仍要看配置、版本和你允许的标签属性范围。真正安全的做法是“尽量不渲染不可信 HTML”,必须渲染时再做白名单消毒。

6. 前端最容易引入 XSS 的 API 是哪些?

innerHTMLouterHTMLinsertAdjacentHTML、拼事件属性、危险的富文本回填。这类 API 的共同点是会把字符串重新交给浏览器解释。


易错点

  • 把“防止 Cookie 被读到”误当成“防住了 XSS”。
  • 以为“接口是 POST,所以不会 CSRF”。方法类型不是核心,关键是浏览器会不会自动带凭证,以及服务端是否做额外校验。
  • 以为“用了 JWT 就没有 CSRF”。如果 JWT 放在 Cookie 里照样会有 CSRF;只有放在 JS 主动设置的请求头里,才天然更不容易被伪造。
  • 把 CSP 当成万能盾牌。CSP 是缓解措施,不是替代安全编码。
  • 富文本只做黑名单过滤。安全上应优先白名单,不要赌漏网标签和属性。

速记要点

  • XSS:不可信内容被执行。
  • CSRF:浏览器自动带凭证导致请求被冒用。
  • 防 XSS 看编码、消毒、CSP、HttpOnly
  • 防 CSRF 看 SameSite、CSRF Token、来源校验。
  • 有 XSS 时,很多 CSRF 防线都会失效。