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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions