Summary
Deserializing certain crafted JSON into a Map via Gson.fromJson(...) reaches com.google.gson.internal.LinkedTreeMap.put with a null key and throws a raw java.lang.NullPointerException with message key == null, instead of one of Gson's documented parse exceptions (JsonSyntaxException / JsonParseException). The map type adapter accepts the JSON array-encoded entry form ([[key, value], ...]) on read regardless of configuration, and when the encoded key is the JSON literal null it is handed directly to the backing map.
This matters because the exception escapes Gson's documented error-handling contract. Gson.fromJson is documented to throw JsonSyntaxException (a JsonParseException) for malformed input, so a caller deserializing untrusted JSON typically guards with catch (JsonParseException ...). A NullPointerException is unchecked and not part of that family, so it slips past such handlers and propagates up the stack, allowing crafted input to crash a caller that otherwise correctly handles malformed JSON (an availability / robustness issue). The JVM remains memory-safe; this is not memory corruption.
Root Cause
For a target Map type, MapTypeAdapterFactory$Adapter.read supports two on-the-wire encodings: a JSON object ({"k": v}) and a JSON array of two-element arrays ([[k, v], ...], the "complex map key" encoding). In the array branch, the key is decoded with the key type adapter and passed straight to map.put(key, value). When the encoded key is the JSON literal null, the key adapter returns null, and the default map implementation, LinkedTreeMap, rejects a null key with a bare NullPointerException:
// com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter#read (array branch)
in.beginArray();
while (in.hasNext()) {
in.beginArray(); // one [key, value] entry
K key = keyTypeAdapter.read(in); // resolves to null for JSON `null`
V value = valueTypeAdapter.read(in);
V replaced = map.put(key, value); // <-- NPE thrown here for null key
if (replaced != null) {
throw new JsonSyntaxException("duplicate key: " + key);
}
in.endArray();
}
in.endArray();
// com.google.gson.internal.LinkedTreeMap#put
@Override
public V put(K key, V value) {
if (key == null) {
throw new NullPointerException("key == null");
}
...
}
Because the null-key rejection happens deep inside the internal map rather than at the adapter boundary, malformed input surfaces as an unchecked NullPointerException instead of a typed JsonSyntaxException.
PoC
The following JSON, deserialized into Map<String, Object>, triggers the bug. It uses the array-encoded map-entry form with a single entry whose key is the JSON literal null:
Reproduction
Build/obtain gson 2.11.0 and run the public API on the input:
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.Map;
public class Repro {
public static void main(String[] args) {
String json = "[[null,1]]";
Type mapType = new TypeToken<Map<String, Object>>() {}.getType();
Object result = new Gson().fromJson(json, mapType); // throws NPE
System.out.println("No exception, result = " + result);
}
}
Compile and run:
javac -cp gson-2.11.0.jar Repro.java
java -cp .:gson-2.11.0.jar Repro
Output:
Exception in thread "main" java.lang.NullPointerException: key == null
at com.google.gson.internal.LinkedTreeMap.put(LinkedTreeMap.java:118)
at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:188)
at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:154)
at com.google.gson.Gson.fromJson(Gson.java:1361)
at com.google.gson.Gson.fromJson(Gson.java:1262)
at com.google.gson.Gson.fromJson(Gson.java:1171)
at com.google.gson.Gson.fromJson(Gson.java:1137)
at Repro.main(Repro.java:11)
Suggested Fix
In MapTypeAdapterFactory$Adapter.read, validate the decoded key before insertion and translate a null key into a typed parse error rather than letting the backing map throw an unchecked NullPointerException, e.g.:
K key = keyTypeAdapter.read(in);
if (key == null) {
throw new JsonSyntaxException("Map key is null");
}
V value = valueTypeAdapter.read(in);
This keeps all malformed-input outcomes inside the documented JsonParseException contract. (The same guard applies to both the array-encoded and object-encoded branches.)
PoC bytes (self-contained)
The trigger JSON is [[null,1]] (10 bytes, plain ASCII). No binary decoding is required; pass the literal string above to Gson.fromJson with target type Map<String, Object> to reproduce.
Credit
Aisle Research (Ze Sheng (O2Lab & TAMU), Dmitrijs Trizna, Luigino Camastra, Guido Vranken).
Summary
Deserializing certain crafted JSON into a
MapviaGson.fromJson(...)reachescom.google.gson.internal.LinkedTreeMap.putwith anullkey and throws a rawjava.lang.NullPointerExceptionwith messagekey == null, instead of one of Gson's documented parse exceptions (JsonSyntaxException/JsonParseException). The map type adapter accepts the JSON array-encoded entry form ([[key, value], ...]) on read regardless of configuration, and when the encoded key is the JSON literalnullit is handed directly to the backing map.This matters because the exception escapes Gson's documented error-handling contract.
Gson.fromJsonis documented to throwJsonSyntaxException(aJsonParseException) for malformed input, so a caller deserializing untrusted JSON typically guards withcatch (JsonParseException ...). ANullPointerExceptionis unchecked and not part of that family, so it slips past such handlers and propagates up the stack, allowing crafted input to crash a caller that otherwise correctly handles malformed JSON (an availability / robustness issue). The JVM remains memory-safe; this is not memory corruption.Root Cause
For a target
Maptype,MapTypeAdapterFactory$Adapter.readsupports two on-the-wire encodings: a JSON object ({"k": v}) and a JSON array of two-element arrays ([[k, v], ...], the "complex map key" encoding). In the array branch, the key is decoded with the key type adapter and passed straight tomap.put(key, value). When the encoded key is the JSON literalnull, the key adapter returnsnull, and the default map implementation,LinkedTreeMap, rejects anullkey with a bareNullPointerException:Because the
null-key rejection happens deep inside the internal map rather than at the adapter boundary, malformed input surfaces as an uncheckedNullPointerExceptioninstead of a typedJsonSyntaxException.PoC
The following JSON, deserialized into
Map<String, Object>, triggers the bug. It uses the array-encoded map-entry form with a single entry whose key is the JSON literalnull:Reproduction
Build/obtain gson 2.11.0 and run the public API on the input:
Compile and run:
Output:
Suggested Fix
In
MapTypeAdapterFactory$Adapter.read, validate the decoded key before insertion and translate anullkey into a typed parse error rather than letting the backing map throw an uncheckedNullPointerException, e.g.:This keeps all malformed-input outcomes inside the documented
JsonParseExceptioncontract. (The same guard applies to both the array-encoded and object-encoded branches.)PoC bytes (self-contained)
The trigger JSON is
[[null,1]](10 bytes, plain ASCII). No binary decoding is required; pass the literal string above toGson.fromJsonwith target typeMap<String, Object>to reproduce.Credit
Aisle Research (Ze Sheng (O2Lab & TAMU), Dmitrijs Trizna, Luigino Camastra, Guido Vranken).