Skip to content

Adding cap to numeric literals into BigDecimal #1075

Description

@Alexender676

Summary

Jayway JsonPath accepts oversized numeric literals in filter expressions and parses them into BigDecimal during path compilation without a numeric-token length guard. An application that exposes JSONPath filtering to clients can be forced to spend unbounded CPU and allocation work before producing a result, making request-worker exhaustion practical with repeated oversized filters. This includes security relevant effects but I was not able to find any private channels to report this which is why I decided to file it via github issue.

Affected

Root cause

The public string entry point JsonPath.read(String, String, Predicate...) passes the caller-supplied JSONPath text into DocumentContext.read at json-path/src/main/java/com/jayway/jsonpath/JsonPath.java:549. JsonContext.read only rejects an empty path before looking it up in the path cache at json-path/src/main/java/com/jayway/jsonpath/internal/JsonContext.java:75; on a cache miss, pathFromCache compiles the supplied path at json-path/src/main/java/com/jayway/jsonpath/internal/JsonContext.java:221. The compiler accepts inline filter tokens and forwards the complete filter criteria to FilterCompiler.compile at json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java:478, which creates a compiler over that attacker-controlled filter string at json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java:52. Numeric operands fall through readLiteral into readNumberLiteral at json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java:108; readNumberLiteral then advances across every numeric character with no maximum length check, slices the full token, and passes it to ValueNode.createNumberNode at json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java:328 and json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java:333. The sink is NumberNode(CharSequence), which calls new BigDecimal(num.toString()) on the full attacker-sized token at json-path/src/main/java/com/jayway/jsonpath/internal/filter/ValueNodes.java:286 and json-path/src/main/java/com/jayway/jsonpath/internal/filter/ValueNodes.java:287. This is the same structural oversized numeric-token resource exhaustion pattern as GHSA-72hv-8253-57qq, with the JSONPath filter compiler as the parser front end and BigDecimal construction as the expensive sink.

PoC

with an arbitrary json doc.json and a text file with a large number large-number.txt, run the java below

import com.jayway.jsonpath.JsonPath;

import java.nio.file.Files;
import java.nio.file.Path;

public class JaywayBigDecimalProbe {
    public static void main(String[] args) throws Exception {
        Path inputs = Path.of(args[0]);
        String doc = Files.readString(inputs.resolve("doc.json"));
        String numericLiteral = Files.readString(inputs.resolve("large-number.txt")).trim();
        long started = System.nanoTime();
        Object result = JsonPath.read(doc, "$[?(@.x == " + numericLiteral + ")]");
        long elapsedMs = (System.nanoTime() - started) / 1_000_000L;

        if (numericLiteral.length() != 1024 * 1024) {
            throw new AssertionError("unexpected generated token length=" + numericLiteral.length());
        }
        if (result == null) {
            throw new AssertionError("JsonPath.read returned null");
        }

        System.out.println("JAYWAY_JSONPATH_FILTER_BIGDECIMAL_UNBOUNDED token_digits="
                + numericLiteral.length() + " elapsed_ms=" + elapsedMs
                + " result_class=" + result.getClass().getName());
    }
}

Suggested fix

--- a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java
+++ b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java
@@ -47,6 +47,8 @@
     private static final char PATTERN = '/';
     private static final char IGNORE_CASE = 'i';
 
+    private static final int MAX_NUMBER_LITERAL_LENGTH = 1000;
+
     private CharacterIndex filter;
 
     public static Filter compile(String filterString) {
@@ -327,6 +329,9 @@
 
         while (filter.inBounds() && filter.isNumberCharacter(filter.position())) {
             filter.incrementPosition(1);
+            if (filter.position() - begin > MAX_NUMBER_LITERAL_LENGTH) {
+                throw new InvalidPathException("Number literal length exceeds the maximum allowed length of " + MAX_NUMBER_LITERAL_LENGTH);
+            }
         }
         CharSequence numberLiteral = filter.subSequence(begin, filter.position());
         logger.trace("NumberLiteral from {} to {} -> [{}]", begin, filter.position(), numberLiteral);

Reported by Team Atlanta.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions