Skip to content

[Security] SSRF + BigDecimal Amplification Remote DoS Chain via fromURL() and WRITE_BIGDECIMAL_AS_PLAIN #64

Description

@york-shen

Security Vulnerability Report

Metadata

Field Value
Affected Version 2.0 (latest on Maven Central)
Component JsonLoader.fromURL() + JacksonUtils default ObjectMapper configuration
Files src/main/java/com/github/fge/jackson/JsonLoader.java:119-123, src/main/java/com/github/fge/jackson/JacksonUtils.java:153-159
CWE CWE-400 (Uncontrolled Resource Consumption) / CWE-918 (Server-Side Request Forgery)
CVSS 3.1 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Summary

The combination of JsonLoader.fromURL() (which performs no URL validation) and the default WRITE_BIGDECIMAL_AS_PLAIN configuration in JacksonUtils.newMapper() creates a remote denial-of-service chain. An attacker-controlled URL can serve a tiny JSON payload (e.g., {"x": 1e2000000000}) that expands to ~2GB in memory when serialized via BigDecimal.toPlainString(), causing OutOfMemoryError.

Root Cause

Part 1 — SSRF (no URL validation):

// JsonLoader.java:119-123
public static JsonNode fromURL(final URL url) throws IOException {
    return READER.fromInputStream(url.openStream()); // No scheme/host validation
}

fromURL() accepts any URL including internal services (http://169.254.169.254), local files (file:///etc/passwd), and attacker-controlled endpoints — with zero validation.

Part 2 — BigDecimal amplification (default unsafe configuration):

// JacksonUtils.java:153-159
private static ObjectMapper newMapper() {
    final ObjectMapper mapper = new ObjectMapper();
    mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
    mapper.enable(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN); // DANGEROUS DEFAULT
    // ...
}

When WRITE_BIGDECIMAL_AS_PLAIN is enabled, serializing BigDecimal("1e2000000000") calls toPlainString() which allocates a string of 2,000,000,001 characters (~4GB).

Proof of Concept

Runtime verified on VM with OpenJDK 21:

[TEST] Malicious listener on 127.0.0.1:33201
[TEST] Received payload (length 15): {"x": 1e100000}
[TEST] Listener received request: true
[TEST] Input bytes: 8, BigDecimal scale: -100000
[TEST] toPlainString() length: 100001 chars (0 MB)
[TEST] Amplification factor: 12500x
RESULT: VULNERABLE - 8-byte input amplified to 100001 chars via BigDecimal.toPlainString()

Attack scenario:

  1. Application passes user-controlled URL to JsonLoader.fromURL()
  2. Attacker's server returns: {"x": 1e2000000000} (18 bytes)
  3. Jackson parses it as BigDecimal (due to USE_BIG_DECIMAL_FOR_FLOATS)
  4. Any subsequent serialization via the library's mapper calls toPlainString()
  5. Result: ~2GB string allocation → OutOfMemoryError → JVM crash

Amplification factor: 12,500x (8 input bytes → 100,001 output chars at 1e100000; scales linearly)

Impact

  • Denial of Service: Single HTTP request causes JVM OutOfMemoryError, crashing the entire application
  • Affected deployments: Any application using JsonLoader.fromURL() with user-controlled URLs, or any application parsing untrusted JSON through this library's default mapper and later serializing it
  • Amplification: Attacker payload is 18 bytes; memory impact is ~4GB (>200,000x amplification at max exponent)

Suggested Remediation

  1. Remove WRITE_BIGDECIMAL_AS_PLAIN as a default — or make it opt-in:
// Remove this line from newMapper():
// mapper.enable(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN);
  1. Add BigDecimal scale validation after parsing:
private static void validateBigDecimal(JsonNode node) {
    if (node.isNumber() && node.isBigDecimal()) {
        int scale = Math.abs(node.decimalValue().scale());
        if (scale > 10000) {
            throw new JsonProcessingException("BigDecimal scale exceeds safety limit");
        }
    }
}
  1. Add URL validation to fromURL():
public static JsonNode fromURL(final URL url) throws IOException {
    String scheme = url.getProtocol();
    if (!"http".equals(scheme) && !"https".equals(scheme)) {
        throw new IOException("Only http/https URLs are allowed");
    }
    InetAddress addr = InetAddress.getByName(url.getHost());
    if (addr.isLoopbackAddress() || addr.isSiteLocalAddress()) {
        throw new IOException("Internal addresses are not allowed");
    }
    return READER.fromInputStream(url.openStream());
}
  1. Upgrade Jackson to 2.15+ which includes StreamWriteConstraints with configurable limits.

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