Skip to content

Uncaught NullPointerException ("key == null") when deserializing a JSON object with a null key into a Map #3037

Description

@OwenSanzas

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:

[[null,1]]

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions