Skip to content

[BUG] ArrayIndexOutOfBoundsException on a truncated exponent — JSON.parse("1e") #7807

Description

@Namron2000

Version: 2.0.64 (current release, reproduced against the published jar)
Classes: com.alibaba.fastjson2.JSONReaderUTF8, com.alibaba.fastjson2.JSONReaderUTF16
Related: #3883 (closed, fixed in 2.0.61) — this is the same method, three reads it missed
Related: #7790 (open) — same class of defect, different method

Parsing a JSON document that ends inside a number's exponent throws
ArrayIndexOutOfBoundsException instead of JSONException.

JSON.parse("1e");
java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
    at com.alibaba.fastjson2.JSONReaderUTF8.readNumber0(JSONReaderUTF8.java:5487)
    at com.alibaba.fastjson2.JSONReader.readNumber(JSONReader.java:2259)
    at com.alibaba.fastjson2.JSONReader.read(JSONReader.java:3531)
    at com.alibaba.fastjson2.JSON.parse(JSON.java:138)

Two characters are enough. All of these throw the same way:

input line
1e, 1E, -2.2e, [1e, {"a":1e JSONReaderUTF8.java:5487
1e- JSONReaderUTF8.java:5491
1e+ JSONReaderUTF8.java:5493
["中",1e (UTF16-backed input) JSONReaderUTF16.java:4008

This matters because ArrayIndexOutOfBoundsException is not something a caller can be
expected to catch. The documented contract for malformed input is JSONException, so
code written as

try { JSON.parse(untrusted); } catch (JSONException e) { /* reject */ }

is bypassed, and the exception propagates as an unchecked runtime error.

Cause

readNumber0() reads the character after e/E without checking whether the buffer
still has one — JSONReaderUTF8.java:5484-5494:

if (ch == 'e' || ch == 'E') {
    boolean negativeExp = false;
    int expValue = 0;
    ch = bytes[offset++];             // 5487 -- no bounds check

    if (ch == '-') {
        negativeExp = true;
        ch = bytes[offset++];         // 5491 -- no bounds check
    } else if (ch == '+') {
        ch = (char) bytes[offset++];  // 5493 -- no bounds check
    }

Every other read in the same method guards first — the pattern appears 16 times in
readNumber0() alone, including in the exponent's own digit loop eleven lines below at
5504:

if (offset == end) {
    ch = EOI;
    break;
}
ch = bytes[offset++];

So the three reads above are the only ones in this method that skip the check.

Fix

--- a/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF8.java
+++ b/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF8.java
@@ -5484,13 +5484,13 @@
         if (ch == 'e' || ch == 'E') {
             boolean negativeExp = false;
             int expValue = 0;
-            ch = bytes[offset++];
+            ch = offset == end ? EOI : (char) bytes[offset++];
 
             if (ch == '-') {
                 negativeExp = true;
-                ch = bytes[offset++];
+                ch = offset == end ? EOI : (char) bytes[offset++];
             } else if (ch == '+') {
-                ch = (char) bytes[offset++];
+                ch = offset == end ? EOI : (char) bytes[offset++];
             }

offset == end ? EOI : bytes[offset++] is the idiom already used at
JSONReaderUTF8.java:5535 and elsewhere in this class.

JSONReaderUTF16.readNumber0() carries the identical omission at lines 4008, 4012 and
4014 — character for character the same block, over chars[] instead of bytes[] — and
fix.patch fixes both. JSONReaderASCII extends JSONReaderUTF8 and inherits the fix.

Deliberately not adding strictness

A digit-less exponent that is not truncated is already accepted today:

JSON.parse("{\"a\":1e}");   // -> {"a":1}
JSON.parse("[1e+]");        // -> [1]

The patch keeps that behaviour — with it, 1e parses to 1, matching [1e][1].
Making a digit-less exponent an error would be a defensible separate change, but it is a
behaviour change rather than a bug fix, so it is left out here.

Verification

Built the patched class against the 2.0.64 jar and ran both.

Fixed:

input before after
1e, 1E AIOOBE at 5487 1
-2.2e AIOOBE at 5487 -2.2
1e- AIOOBE at 5491 1
1e+ AIOOBE at 5493 1
[1e, {"a":1e AIOOBE at 5487 JSONException (unterminated)

Unchanged:

input before and after
{"a":1e} {"a":1}
[1e], [1e+], [1e-] [1]
[1e5] [100000.0]
1.5e-3 0.0015
1e309 Infinity
123456789012345678901234567890 unchanged
[1,2,3], {"a":1} unchanged

This is what #3883 missed

#3883"Multiple
ArrayIndexOutOfBoundsException in JSONReaderUTF8.java, JSONReaderASCII.java,
JSONReader.java"
, also found by fuzzing — was closed on 2026-02-07 as fixed in 2.0.61.
It reported the same defect in the same method: readNumber0() reading past the end
of a truncated document, triggered by JSON.parse("\0.").

That fix works. On 2.0.64:

JSON.parse("\0.");   // JSONException at readNumber0:5441   <- #3883, fixed
JSON.parse("1e");    // ArrayIndexOutOfBoundsException at readNumber0:5487   <- still broken

The guard was added to the reads around the decimal point, but the three reads that
consume the character after e/E — lines 5487, 5491 and 5493 of the same method —
were not touched. So this is not a new defect so much as the remainder of #3883.

One case from #3883 is also still open. Its comment thread reports
JSON.parse("[[N"), which was never fixed and still throws on 2.0.64:

java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
    at com.alibaba.fastjson2.JSONReaderUTF8.readNaN(JSONReaderUTF8.java:6006)

Since #3883 is closed, that one is no longer tracked anywhere.

Relationship to issue #7790

#7790 (open, 2.0.64) reports
ArrayIndexOutOfBoundsException from JSONReaderUTF16.readString() at line 3287 on a
truncated string escape ("\). Same class of defect, different site — not a
duplicate:

#7790 this issue
reader JSONReaderUTF16 JSONReaderUTF8 (and JSONReaderUTF16)
method readString() readNumber0()
line 3287 5487 / 5491 / 5493 (UTF8), 4008 / 4012 / 4014 (UTF16)
trigger input ends after \ in a string input ends after e/E in a number

Both were confirmed against 2.0.64 here, and each is unaffected by the other's fix.

Two things worth noting for #7790's framing. Its report says "the UTF8 reader correctly
throws JSONException for the same input", suggesting UTF16 is the outlier — but the
truncated-exponent case shows the UTF8 reader has the same defect in a different
method
, so this is a pattern across readers rather than one reader lagging behind.

Also, reproducing #7790 from a Java String requires the string to be UTF16-backed. An
ASCII-only String is latin1-backed on modern JVMs, so JSON.parse("\"\\") takes the
UTF8/ASCII path and correctly throws JSONException; adding a non-latin1 character
(JSON.parse("\"\u4e2d\\")) routes it to JSONReaderUTF16 and reproduces the AIOOBE.
That may explain any difficulty reproducing it.

The same omission exists at several other sites

Fuzzing this parser produced ArrayIndexOutOfBoundsException at six further places. Each
was delta-debug minimised to the shortest input that keeps the exact same crash line.
Every one is a truncated document — the input stops where the parser assumed another
character — so they look like the same missing bounds check in different methods:

crash site minimal reproducer what is truncated
JSONReaderUTF8:5979 {"":[false],"":[true],"":[""n a null / true literal
JSONReaderASCII:982 {"":"","":null,"":{R{"": stops after ":
JSONReaderASCII:1401 {"":[],"":"\\\"\ a string escape
JSONReaderASCII:949 {"":7,"":8,"":{"\ a string escape
JSONReaderASCII:876 [{\"\\t\u00e0\u00aa\u00ae\": (Java literal) stops after :
JSONReader:1342 (not reproduced through JSON.parse(String))

Each reproducer above was re-run against 2.0.64 and confirmed to produce the crash line
shown. The patch in this issue fixes only the exponent case; the rest still throw.

Note that the two JSONReaderASCII escape cases (:949, :1401) are the same trigger
as #7790
— a string ending in a lone \ — just in a different reader.

Counting #7790, that is at least seven places where the guarded
offset == end ? EOI : buf[offset++] idiom is missing. Rather than filing these
one at a time, a sweep of the unguarded bytes[offset++] / chars[offset++] reads in
JSONReaderUTF8, JSONReaderASCII and JSONReaderUTF16 would likely close all of them
at once.

Found by the CISPA Fandango Team

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions