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、关键操作二次确认。
- 防 XSS:输出编码、避免危险 DOM API、内容消毒、CSP、敏感 Cookie 加
- 常见追问:
HttpOnly能防 XSS 窃 Cookie,但不能阻止 XSS 直接在当前站内发请求;所以 XSS 一旦成立,很多 CSRF 防线也会被绕过。
心智模型
可以把这两个攻击理解成两条完全不同的入侵路径:
一句话记忆:
- XSS 是“脚本进来了”
- CSRF 是“请求被冒用了”
一张表讲清差异
| 维度 | XSS | CSRF |
|---|---|---|
| 攻击目标 | 让站点执行攻击者脚本 | 让站点接收攻击者伪造请求 |
| 前提条件 | 页面存在注入点 | 用户已登录且浏览器会自动带凭证 |
| 利用媒介 | HTML、JS、DOM、模板渲染 | Cookie、自动登录态、跨站请求 |
| 是否需要读响应 | 往往不需要,能执行 JS 就够危险 | 通常也不需要读响应,只要请求成功 |
| 典型影响 | 窃取信息、劫持页面、伪造操作 | 冒用身份发起敏感操作 |
| 核心防线 | 编码、消毒、CSP、HttpOnly | SameSite、CSRF Token、来源校验 |
XSS:为什么会发生
1. 根因
当你把“不可信字符串”放进一个会被浏览器当成 HTML 或 JS 解释的位置,就可能触发 XSS。
常见高危位置:
innerHTMLouterHTMLinsertAdjacentHTML- 动态拼接事件属性,如
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
问题不一定出在服务端模板,而是前端脚本自己从 location、document.cookie、postMessage 等位置取值,再危险拼接到 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 的请求路径
为什么 Token 方案通常比 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';
它的价值在于:
- 限制脚本来源
- 尽量禁用内联脚本
- 降低注入后立即执行的概率
5. 敏感 Cookie 用 HttpOnly
这样即使出现 XSS,攻击脚本也不能直接通过 document.cookie 读出认证 Cookie。
6. 减少前端可接触的高价值数据
例如:
- 不把长期有效密钥放在
localStorage - 页面只保留当前渲染必须的数据
- 退出登录时清理内存和缓存
如何防 CSRF
1. SameSite
现代浏览器里,SameSite 已经是第一层基础防线。
Strict:最严,几乎所有跨站场景都不带 CookieLax:兼顾体验,能拦住大多数跨站子请求和跨站 POSTNone:允许跨站带 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 是哪些?
答:innerHTML、outerHTML、insertAdjacentHTML、拼事件属性、危险的富文本回填。这类 API 的共同点是会把字符串重新交给浏览器解释。
易错点
- 把“防止 Cookie 被读到”误当成“防住了 XSS”。
- 以为“接口是 POST,所以不会 CSRF”。方法类型不是核心,关键是浏览器会不会自动带凭证,以及服务端是否做额外校验。
- 以为“用了 JWT 就没有 CSRF”。如果 JWT 放在 Cookie 里照样会有 CSRF;只有放在 JS 主动设置的请求头里,才天然更不容易被伪造。
- 把 CSP 当成万能盾牌。CSP 是缓解措施,不是替代安全编码。
- 富文本只做黑名单过滤。安全上应优先白名单,不要赌漏网标签和属性。
速记要点
- XSS:不可信内容被执行。
- CSRF:浏览器自动带凭证导致请求被冒用。
- 防 XSS 看编码、消毒、CSP、
HttpOnly。 - 防 CSRF 看
SameSite、CSRF Token、来源校验。 - 有 XSS 时,很多 CSRF 防线都会失效。