[BUG] `\x` escape indexes `DIGITS2` without bounds checking — `JSON.parse("\"\\xzz\"")` throws AIOOBE
- 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
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