JSON 劫持(JSON Hijacking):利用旧版浏览器漏洞读取跨域 JSON 数据

JSON 劫持(JSON Hijacking):利用旧版浏览器漏洞读取跨域 JSON 数据 —— 技术深度解析与防御指南

大家好,欢迎来到今天的专题讲座。我是你们的技术讲师,今天我们要深入探讨一个在 Web 安全领域曾经非常危险、但如今仍值得警惕的漏洞类型:JSON 劫持(JSON Hijacking)

这不是一个过时的话题。虽然现代浏览器已经修复了大部分相关漏洞,但在某些遗留系统、老旧设备或配置不当的环境中,它依然可能被攻击者利用。理解它的原理和防范方法,是每一位前端开发者、后端工程师以及安全人员必须掌握的基本功。


一、什么是 JSON 劫持?

定义

JSON 劫持是一种 跨域攻击技术,其核心思想是:

攻击者通过构造恶意脚本,在受害者的浏览器中执行一段代码,从而窃取目标网站返回的 JSON 数据——即使这些数据来自另一个域名(即跨域)。

这听起来像“跨站请求伪造”(CSRF),但本质不同:

  • CSRF 是伪造请求行为(如修改账户信息)。
  • JSON 劫持则是直接读取响应内容(如获取用户数据、API 密钥等)。

核心前提:为什么能成功?

关键在于:早期浏览器对 JSON 的处理方式存在缺陷

具体来说:

  • 在 2008 年以前,许多浏览器(尤其是 IE6/IE7)会将 JSON 响应当作 JavaScript 来解析。
  • 如果服务器返回的是纯 JSON(如 {"user":"alice","token":"xxx"}),而没有设置合适的 Content-Type 或 CORS 头部,浏览器就会尝试将其作为脚本执行。
  • 这就给了攻击者机会:他们可以注入一个 <script> 标签指向该接口,并在页面上定义同名变量来捕获数据!

二、历史背景与经典案例

让我们用时间线来回顾一下这个漏洞的发展:

时间 关键事件 影响
2005–2008 JSON 成为主流数据格式 开始广泛使用,但缺乏安全性设计
2007 Google、Yahoo! 等公司暴露 JSON 劫持风险 社区开始关注此问题
2008 IE6/IE7 漏洞公开 攻击者可轻松劫持敏感数据
2010+ 浏览器厂商逐步修复 如 Chrome、Firefox 加强了 JSON 解析逻辑

📌 典型案例:
假设你有一个 API 接口 /api/user 返回如下内容:

{
  "username": "alice",
  "email": "[email protected]",
  "session_token": "abc123xyz"
}

如果攻击者知道这个 URL,并且你的网站允许跨域访问(比如没设 Access-Control-Allow-Origin),他可以在自己的页面里写:

<script src="https://your-site.com/api/user"></script>
<script>
  var user = { /* 被劫持的数据 */ };
  // 此时 user 对象已被填充!
  alert(user.username); // 可以弹出用户名
</script>

这就是典型的 JSON 劫持攻击流程。


三、代码演示:如何模拟一次 JSON 劫持攻击?

我们分两步走:先搭建一个“易受攻击”的服务端接口,再编写客户端攻击脚本。

Step 1:模拟服务端(Node.js + Express)

创建一个简单的 Express 应用,用于返回 JSON 数据:

// server.js
const express = require('express');
const app = express();

app.use(express.static('public'));

// ❗️这是易受攻击的接口:未设置 Content-Type 或 CORS
app.get('/api/user', (req, res) => {
  res.send(`{"username":"alice","email":"[email protected]","token":"secret123"}`);
});

app.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});

注意这里的问题:

  • 没有设置 Content-Type: application/json
  • 没有启用 CORS(默认不允许跨域)
  • 直接发送原始字符串,浏览器会按 JS 执行!

Step 2:攻击者页面(HTML + JS)

现在我们模拟攻击者发起请求:

<!-- attacker.html -->
<!DOCTYPE html>
<html>
<head>
  <title>JSON Hijack Demo</title>
</head>
<body>
  <h2>攻击者页面</h2>
  <p>正在尝试窃取目标用户的 JSON 数据...</p>

  <!-- 注入目标接口 -->
  <script src="http://localhost:3000/api/user"></script>

  <!-- 定义全局变量,覆盖目标 JSON 内容 -->
  <script>
    // 注意:这里的变量名要和 JSON 中的 key 匹配吗?不需要!
    // 因为我们是在全局作用域下声明了一个对象!
    var hijackedData = null;

    // 当 JSON 被解析时,会触发赋值操作
    window.onload = function() {
      if (typeof hijackedData !== 'undefined') {
        alert("已劫持数据:" + JSON.stringify(hijackedData));
      } else {
        alert("未劫持成功!");
      }
    };
  </script>
</body>
</html>

⚠️ 重要提示:
这段代码本身不会自动运行,除非你在浏览器中手动打开 attacker.html,并且目标网站(localhost:3000)允许跨域访问(当前环境满足条件)。

✅ 实际效果:

  • 浏览器加载 attacker.html
  • 自动加载 /api/user 的 JSON
  • JSON 被当作脚本执行 → 创建全局变量 {"username":...}
  • 最终攻击者可以通过 window.username 或其他方式获取数据!

四、为什么会发生?底层机制详解

4.1 浏览器如何解析 JSON?

在现代浏览器中,如果你发送一个请求并收到 JSON,通常有两种情况:

请求方式 浏览器行为
使用 fetch() / XMLHttpRequest 不会自动执行 JSON,而是返回文本或对象(需手动 .json() 解析)
使用 <script src="..."> 若响应内容是合法 JS,会被当作脚本执行(包括 JSON!)

💡 所以关键是:JSON 是否被视为有效的 JavaScript 表达式?

例如:

{"name":"alice"}

这其实是一个合法的 JavaScript 对象字面量!所以浏览器会认为它是脚本,而不是纯文本。

👉 因此,只要攻击者能让受害者浏览器加载你的 JSON 数据为 <script>,就能拿到里面的内容!

4.2 为什么现代浏览器不再受影响?

从 2010 年起,主流浏览器做了以下改进:

浏览器 行动
Chrome 强制检查 Content-Type,若非 application/json,则不执行脚本
Firefox 类似 Chrome,增强 MIME 类型验证
IE9+ 不再把 JSON 当作脚本执行(除非显式指定)

这意味着:现在的 JSON 劫持需要配合其他漏洞(如 XSS、CORS 配置错误)才能成功


五、常见攻击场景总结

场景 描述 危险等级
无 CORS 设置的 JSON 接口 攻击者可通过 <script> 获取数据 ⭐⭐⭐⭐
JSONP 接口未加校验 攻击者可伪造回调函数名称 ⭐⭐⭐⭐
同源策略失效 如 iframe 中嵌套跨域页面 ⭐⭐⭐
用户登录态持久化 JSON 包含 token、session 等敏感字段 ⭐⭐⭐⭐⭐

📌 特别提醒:即使你用了 JSONP(一种老式的跨域解决方案),也必须确保回调函数名可控,否则同样容易被劫持!


六、如何防御 JSON 劫持?

✅ 方法一:设置正确的 Content-Type

服务端必须明确告诉浏览器:“我返回的是 JSON,不是脚本”。

// Express 示例
app.get('/api/user', (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  res.json({
    username: 'alice',
    email: '[email protected]',
    token: 'secret123'
  });
});

这样浏览器就不会把它当作脚本执行了。

✅ 方法二:启用 CORS 白名单(推荐)

不要让任意站点都能访问你的接口:

app.use(cors({
  origin: ['https://trusted-domain.com'], // 白名单
  credentials: true
}));

或者更严格的模式:

app.use((req, res, next) => {
  const allowedOrigins = ['https://trusted-domain.com'];
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
  }
  next();
});

✅ 方法三:避免返回裸 JSON 字符串(建议)

不要直接返回 res.send('{...}'),应该使用框架提供的 .json() 方法:

❌ 错误做法:

res.send(`{"username":"alice"}`);

✅ 正确做法:

res.json({ username: "alice" });

这样生成的响应体才是结构化的 JSON,不容易被误识别为脚本。

✅ 方法四:使用 JSONP 时添加签名验证(如果必须用)

如果你坚持要用 JSONP(比如兼容老 IE),请务必加上随机 token 和签名机制:

// 服务端生成唯一 callback 名称 + token
function handleJsonp(req, res) {
  const token = Math.random().toString(36).substring(2, 15);
  res.jsonp({
    data: { ... },
    token: token
  });
}

// 客户端校验 token
window.callback = function(data) {
  if (data.token === expectedToken) {
    console.log("数据可信");
  } else {
    console.error("数据不可信!");
  }
};

但这只是权宜之计,建议优先改造成标准 REST API + CORS。


七、真实世界中的教训与反思

🧠 教训一:不要信任任何输入来源

哪怕是你自己的内部 API,也要考虑跨域访问的可能性。很多企业内网系统因为开放了 JSON 接口而被外部渗透。

🧠 教训二:防御不能只靠单一手段

  • 单纯加 CORS 不够 → 必须结合 Content-Type、身份认证、权限控制;
  • 单纯禁用 <script> 不现实 → 现代应用离不开动态加载;
  • 所以防御要多层叠加:协议层(HTTPS)、传输层(CORS)、应用层(数据脱敏)。

🧠 教训三:持续更新依赖库 & 浏览器版本

有些项目仍在使用 Node.js v6.x 或更低版本,它们默认的 Express 行为可能不符合最新安全规范。保持升级是预防此类漏洞的基础。


八、结语:JSON 劫持虽老,警钟长鸣

今天我们详细讲解了 JSON 劫持的历史、原理、攻击手法以及防御策略。虽然它不像 SQL 注入那样频繁出现在新闻中,但它曾是 Web 安全领域的重大威胁之一。

对于今天的开发者而言:

  • 不必恐慌,但也不能忽视;
  • 理解其背后的安全逻辑,有助于构建更健壮的系统;
  • 尤其是在维护遗留系统时,更要小心这类“隐形炸弹”。

记住一句话:安全不是一次性的工作,而是一场永不停歇的博弈。

希望今天的分享对你有所帮助!如果你有任何疑问,欢迎留言讨论。谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注