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 安全领域的重大威胁之一。
对于今天的开发者而言:
- 不必恐慌,但也不能忽视;
- 理解其背后的安全逻辑,有助于构建更健壮的系统;
- 尤其是在维护遗留系统时,更要小心这类“隐形炸弹”。
记住一句话:安全不是一次性的工作,而是一场永不停歇的博弈。
希望今天的分享对你有所帮助!如果你有任何疑问,欢迎留言讨论。谢谢大家!