alibaba / alibaba/fastjson2

[BUG] `\x` escape indexes `DIGITS2` without bounds checking — `JSON.parse("\"\\xzz\"")` throws AIOOBE

Open Beginner friendly
#7,810 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Java
Stars
4.4k
Forks
613
Avg merge
1d 22h
Merged PRs (30d)
6

Description

**Version:** 2.0.64 (reproduced against the published jar)
**Class:** `com.alibaba.fastjson2.JSONReader`

A `\x` escape followed by any character outside the hex-digit table throws
`ArrayIndexOutOfBoundsException` instead of parsing or raising `JSONException`. Six bytes
are enough:

```java
JSON.parse("\"\\xzz\"");
```

```
java.lang.ArrayIndexOutOfBoundsException: Index 122 out of bounds for length 103
at com.alibaba.fastjson2.JSONReader.char2(JSONReader.java:1342)
```

## Cause

`JSONReader.char2()` (line 1341) indexes the table with the raw character:

```java
static char char2(int c1, int c2) {
return (char) (DIGITS2[c1] * 0x10
+ DIGITS2[c2]);
}
```

`DIGITS2` (`JSONFactory.java:142`) has 103 entries, covering `0` through `'f'` (102).
Any escape character above that — or negative — indexes out of bounds:

| input | index | note |
|---|---|---|
| `"\xgg"` | 103 | `'g'`, the first character past the table |
| `"\xzz"` | 122 | `'z'` |
| `"\x{{"` | 123 | `'{'` |
| `"\xÿÿ"` | **-1** | byte `0xFF` read as a signed byte |
| `"\xΩΩ"` | 937 | U+03A9 |

Note the negative index: in the byte-based readers a character ≥ 0x80 arrives as a
negative `int`, so the access can go *below* the array as well as above it.

`char1()`, immediately above at line 1329, does guard itself:

```java
byte b = CHAR1_ESCAPED[c & 0x7f];
if (b == -1) {
throw char1Error(c);
}
```

It masks the index into range and validates the result. `char2()` does neither.

## Reachable everywhere a `\x` escape is allowed

Field names, string values, and any container:

```java
JSON.parse("\"\\xzz\""); // AIOOBE
JSON.parse("[\"\\xzz\"]"); // AIOOBE
JSON.parse("{\"a\":\"\\xzz\"}"); // AIOOBE
JSON.parse("{\"\\xzz\":1}"); // AIOOBE
```

## Fix

`DIGITS2` has no sentinel for invalid entries — in-range non-hex characters already map to
`0`, so `"\x::"` parses today as `\u0000`. The patch extends exactly that behaviour to
characters outside the table, which keeps current semantics intact:

```diff
--- a/core/src/main/java/com/alibaba/fastjson2/JSONReader.java
+++ b/core/src/main/java/com/alibaba/fastjson2/JSONReader.java
@@ -1339,11 +1339,20 @@
static char char2(int c1, int c2) {
- return (char) (DIGITS2[c1] * 0x10
- + DIGITS2[c2]);
+ return (char) (digit2(c1) * 0x10
+ + digit2(c2));
}

+ /**
+ * Hex value of a \x escape digit. DIGITS2 only covers 0..'f'; characters beyond it are
+ * not hex digits either, so they get the same value the table already gives in-range
+ * non-hex characters (0) rather than indexing out of bounds.
+ */
+ private static int digit2(int c) {
+ return c >= 0 && c < DIGITS2.length ? DIGITS2[c] : 0;
+ }
```

Verified against 2.0.64 — every crashing input above now yields `\u0000`, matching how
`"\x::"` and `"\x@@"` already behave, and these are unchanged: `"\x41"` → `A`,
`"\xff"` → `\u00ff`, `"\x00"` → `\u0000`, `"\x::"` → `\u0000`, `"\x@@"` → `\u0000`,
`{"\x41":1}` → `{"A":1}`, `"\u0041"` → `A`, `{"a":1}`, `[1,2,3]`.

**If you would rather reject invalid escapes** than silently decode them to `\u0000`,
that is a defensible alternative — `char2()` would throw a `JSONException` the way
`char1Error()` does. It is a behaviour change for `"\x::"` and friends as well as for the
crashing inputs, which is why the patch above takes the conservative route. Happy to
switch it if you prefer the strict version.

## Not a security issue

The out-of-range access is a read and the JVM bounds check stops it — nothing is read or
disclosed. Impact is an unchecked `ArrayIndexOutOfBoundsException` escaping callers that
follow the documented contract of catching `JSONException`.
---
Found by the CISPA Fandango-Team

Contributor guide

Open the contributing guide

Research direction

Start in core/src/main/java/com/alibaba/fastjson2/JSONReader.java at char2(), then inspect DIGITS2 in JSONFactory.java. Reproduce the JSON.parse examples containing invalid \x escapes and compare them with valid escapes and existing in-range invalid characters. Done means out-of-range inputs no longer escape as ArrayIndexOutOfBoundsException while valid parsing and existing behavior remain covered by regression tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
85/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.