Version: 2.0.64 (latest release, reproduced against the published jar)
Classes: com.alibaba.fastjson2.JSONReaderASCII, com.alibaba.fastjson2.JSONReaderUTF16
A document that ends in whitespace just after a field name throws
ArrayIndexOutOfBoundsException instead of JSONException. Five bytes of plain ASCII:
java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
at com.alibaba.fastjson2.JSONReaderASCII.readFieldName(JSONReaderASCII.java:966)
at com.alibaba.fastjson2.JSONReader.readObject(JSONReader.java:3739)
at com.alibaba.fastjson2.JSON.parse(JSON.java:142)
Without the trailing space, JSON.parse("{\"a\"") raises JSONException correctly — which
is the behaviour expected here.
Cause
readFieldName() reads the character after the closing quote with a bounds check, then
skips whitespace without one (JSONReaderASCII.java:956-970):
offset++;
if (offset < end) { // guarded
c = bytes[offset];
} else {
c = EOI;
}
while (c <= ' ' && ((1L << c) & SPACE) != 0) {
offset++;
c = bytes[offset]; // <-- line 966, no bounds check
}
if (c != ':') {
throw syntaxError(offset, ch);
}
The guarded read sets c correctly at end-of-input. The loop then advances and reads
again unchecked. One trailing whitespace character suffices: the guarded read returns it,
the loop is entered, offset becomes end, and bytes[end] is read.
The statement immediately after the loop is guarded again
(c = ++offset == end ? EOI : chars[offset];), so the loop is the only gap in an otherwise
careful sequence.
Four sites, two per reader
The same loop appears twice in readFieldName() — before and after the : — in both
readers that implement the method:
| site |
minimal input |
JSONReaderASCII.readFieldName:966 |
{"a" |
JSONReaderASCII.readFieldName:982 |
{"a": |
JSONReaderUTF16.readFieldName:2008 |
{"中" |
JSONReaderUTF16.readFieldName:2018 |
{"中": |
Any whitespace works — space, \t, \r, \n, or several of them.
Only JSON.parse(String) is affected. JSON.parse(byte[]) goes through
readFieldNameHashCode(), whose equivalent loops are guarded, and returns a clean
JSONException.
The idiom is right 199 times elsewhere
Sweeping the three readers for this loop shape and classifying by whether the body contains
a bounds check:
| file |
guarded |
unguarded |
JSONReaderASCII.java |
11 |
2 |
JSONReaderUTF8.java |
96 |
12 |
JSONReaderUTF16.java |
92 |
20 |
The correct form is used overwhelmingly:
while (ch <= ' ' && ((1L << ch) & SPACE) != 0) {
offset++;
if (offset >= this.end) { ch = EOI; break; }
ch = bytes[offset];
}
Four of the 34 unguarded ones are demonstrated reachable above. The remaining 30 were not
probed to a conclusion — they may well be unreachable, but the count seems worth a look on
your side.
Fix
Guard the four demonstrated sites with the same idiom the other 199 use:
while (c <= ' ' && ((1L << c) & SPACE) != 0) {
offset++;
+ if (offset >= end) {
+ c = EOI;
+ break;
+ }
c = bytes[offset];
}
and identically over chars[] in JSONReaderUTF16. c = EOI then falls into the existing
if (c != ':') throw syntaxError(...), so the truncated document is reported as such.
The attached patch covers only those four, not all 34 — guarding the rest is your call and
wants evidence per site.
Verification
Both readers patched from the 2.0.64 sources and compiled ahead of the published jar.
| case |
stock 2.0.64 |
patched |
{"a" , {"a": , {"中" , {"中": |
AIOOBE |
JSONException |
{"a"\t, {"a"\r, {"a"\n, {"a" , {"中"\r |
AIOOBE |
JSONException |
| the original 133-byte fuzzer input |
AIOOBE |
JSONException |
{"a", {"a":, {"a":1, {"a" x, {"中" |
JSONException |
JSONException |
{"a":1} |
{"a":1} |
{"a":1} |
{"a" : 1} |
{"a":1} |
{"a":1} |
{"a"\r\n:\t1} |
{"a":1} |
{"a":1} |
{"中":1}, {"中" : 1} |
parse |
parse |
[{"a":1}], {"a":1,"b":2}, {} |
parse |
parse |
No behaviour change for any valid input — in particular whitespace between a field name
and its colon keeps working, which is the regression that would matter.
Not a security issue
The out-of-range access is a read, and the JVM's bounds check fires before it. Nothing
is read or disclosed, and the exception message carries only the input length.
The impact is the API-contract one: code following the documented pattern
try { JSON.parse(untrusted); } catch (JSONException e) { /* reject */ }
does not catch this, so five bytes of ASCII propagate an unchecked RuntimeException past
the intended error handling.
Best regards,
The Fandango Cispa Team
Version: 2.0.64 (latest release, reproduced against the published jar)
Classes:
com.alibaba.fastjson2.JSONReaderASCII,com.alibaba.fastjson2.JSONReaderUTF16A document that ends in whitespace just after a field name throws
ArrayIndexOutOfBoundsExceptioninstead ofJSONException. Five bytes of plain ASCII:Without the trailing space,
JSON.parse("{\"a\"")raisesJSONExceptioncorrectly — whichis the behaviour expected here.
Cause
readFieldName()reads the character after the closing quote with a bounds check, thenskips whitespace without one (
JSONReaderASCII.java:956-970):The guarded read sets
ccorrectly at end-of-input. The loop then advances and readsagain unchecked. One trailing whitespace character suffices: the guarded read returns it,
the loop is entered,
offsetbecomesend, andbytes[end]is read.The statement immediately after the loop is guarded again
(
c = ++offset == end ? EOI : chars[offset];), so the loop is the only gap in an otherwisecareful sequence.
Four sites, two per reader
The same loop appears twice in
readFieldName()— before and after the:— in bothreaders that implement the method:
JSONReaderASCII.readFieldName:966{"a"JSONReaderASCII.readFieldName:982{"a":JSONReaderUTF16.readFieldName:2008{"中"JSONReaderUTF16.readFieldName:2018{"中":Any whitespace works — space,
\t,\r,\n, or several of them.Only
JSON.parse(String)is affected.JSON.parse(byte[])goes throughreadFieldNameHashCode(), whose equivalent loops are guarded, and returns a cleanJSONException.The idiom is right 199 times elsewhere
Sweeping the three readers for this loop shape and classifying by whether the body contains
a bounds check:
JSONReaderASCII.javaJSONReaderUTF8.javaJSONReaderUTF16.javaThe correct form is used overwhelmingly:
Four of the 34 unguarded ones are demonstrated reachable above. The remaining 30 were not
probed to a conclusion — they may well be unreachable, but the count seems worth a look on
your side.
Fix
Guard the four demonstrated sites with the same idiom the other 199 use:
while (c <= ' ' && ((1L << c) & SPACE) != 0) { offset++; + if (offset >= end) { + c = EOI; + break; + } c = bytes[offset]; }and identically over
chars[]inJSONReaderUTF16.c = EOIthen falls into the existingif (c != ':') throw syntaxError(...), so the truncated document is reported as such.The attached patch covers only those four, not all 34 — guarding the rest is your call and
wants evidence per site.
Verification
Both readers patched from the 2.0.64 sources and compiled ahead of the published jar.
{"a",{"a":,{"中",{"中":JSONException{"a"\t,{"a"\r,{"a"\n,{"a",{"中"\rJSONExceptionJSONException{"a",{"a":,{"a":1,{"a" x,{"中"JSONExceptionJSONException{"a":1}{"a":1}{"a":1}{"a" : 1}{"a":1}{"a":1}{"a"\r\n:\t1}{"a":1}{"a":1}{"中":1},{"中" : 1}[{"a":1}],{"a":1,"b":2},{}No behaviour change for any valid input — in particular whitespace between a field name
and its colon keeps working, which is the regression that would matter.
Not a security issue
The out-of-range access is a read, and the JVM's bounds check fires before it. Nothing
is read or disclosed, and the exception message carries only the input length.
The impact is the API-contract one: code following the documented pattern
does not catch this, so five bytes of ASCII propagate an unchecked
RuntimeExceptionpastthe intended error handling.
Best regards,
The Fandango Cispa Team