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.
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
Version: 2.0.64 (current release, reproduced against the published jar)
Classes:
com.alibaba.fastjson2.JSONReaderUTF8,com.alibaba.fastjson2.JSONReaderUTF16Related: #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
ArrayIndexOutOfBoundsExceptioninstead ofJSONException.Two characters are enough. All of these throw the same way:
1e,1E,-2.2e,[1e,{"a":1eJSONReaderUTF8.java:54871e-JSONReaderUTF8.java:54911e+JSONReaderUTF8.java:5493["中",1e(UTF16-backed input)JSONReaderUTF16.java:4008This matters because
ArrayIndexOutOfBoundsExceptionis not something a caller can beexpected to catch. The documented contract for malformed input is
JSONException, socode written as
is bypassed, and the exception propagates as an unchecked runtime error.
Cause
readNumber0()reads the character aftere/Ewithout checking whether the bufferstill has one —
JSONReaderUTF8.java:5484-5494: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 at5504:
So the three reads above are the only ones in this method that skip the check.
Fix
offset == end ? EOI : bytes[offset++]is the idiom already used atJSONReaderUTF8.java:5535and elsewhere in this class.JSONReaderUTF16.readNumber0()carries the identical omission at lines 4008, 4012 and4014 — character for character the same block, over
chars[]instead ofbytes[]— andfix.patchfixes both.JSONReaderASCIIextendsJSONReaderUTF8and inherits the fix.Deliberately not adding strictness
A digit-less exponent that is not truncated is already accepted today:
The patch keeps that behaviour — with it,
1eparses to1, 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:
1e,1E1-2.2e-2.21e-11e+1[1e,{"a":1eJSONException(unterminated)Unchanged:
{"a":1e}{"a":1}[1e],[1e+],[1e-][1][1e5][100000.0]1.5e-30.00151e309Infinity123456789012345678901234567890[1,2,3],{"a":1}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 endof a truncated document, triggered by
JSON.parse("\0.").That fix works. On 2.0.64:
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:Since #3883 is closed, that one is no longer tracked anywhere.
Relationship to issue #7790
#7790 (open, 2.0.64) reports
ArrayIndexOutOfBoundsExceptionfromJSONReaderUTF16.readString()at line 3287 on atruncated string escape (
"\). Same class of defect, different site — not aduplicate:
JSONReaderUTF16JSONReaderUTF8(andJSONReaderUTF16)readString()readNumber0()\in a stringe/Ein a numberBoth 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
Stringrequires the string to be UTF16-backed. AnASCII-only
Stringis latin1-backed on modern JVMs, soJSON.parse("\"\\")takes theUTF8/ASCII path and correctly throws
JSONException; adding a non-latin1 character(
JSON.parse("\"\u4e2d\\")) routes it toJSONReaderUTF16and reproduces the AIOOBE.That may explain any difficulty reproducing it.
The same omission exists at several other sites
Fuzzing this parser produced
ArrayIndexOutOfBoundsExceptionat six further places. Eachwas 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:
JSONReaderUTF8:5979{"":[false],"":[true],"":[""nnull/trueliteralJSONReaderASCII:982{"":"","":null,"":{R{"":":JSONReaderASCII:1401{"":[],"":"\\\"\JSONReaderASCII:949{"":7,"":8,"":{"\JSONReaderASCII:876[{\"\\t\u00e0\u00aa\u00ae\":(Java literal):JSONReader:1342JSON.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
JSONReaderASCIIescape cases (:949,:1401) are the same triggeras #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 theseone at a time, a sweep of the unguarded
bytes[offset++]/chars[offset++]reads inJSONReaderUTF8,JSONReaderASCIIandJSONReaderUTF16would likely close all of themat once.
Found by the CISPA Fandango Team