Skip to content

[BUG] ArrayIndexOutOfBoundsException` on a field name followed by trailing whitespace #7826

Description

@Namron2000

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:

JSON.parse("{\"a\" ");
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

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