Skip to content

EnumMap RPC deserialization throws an uncaught NullPointerException on crafted input #10391

Description

@Nexory

The server custom field serializer for EnumMap instantiates the map from an
untyped readObject(), unlike every other collection serializer in the RPC
core, which read what they instantiate from with a type check.

user/src/com/google/gwt/user/server/rpc/core/java/util/EnumMap_ServerCustomFieldSerializer.java
overrides the type-checking server instantiate but discards the type
information it is given and calls the client serializer:

public EnumMap instantiateInstance(ServerSerializationStreamReader streamReader,
    Type[] expectedParameterTypes, DequeMap<TypeVariable<?>, Type> resolvedTypes)
    throws SerializationException {
  return EnumMap_CustomFieldSerializer.instantiate(streamReader); // untyped
}

EnumMap_CustomFieldSerializer.instantiate then does:

Object exemplar = streamReader.readObject();   // no expected type
Class clazz = exemplar.getClass();
return new EnumMap(clazz);                      // expects an enum class

Compare TreeMap_ServerCustomFieldSerializer / TreeSet_ServerCustomFieldSerializer,
which read their comparator through the typed path
streamReader.readObject(Comparator.class, resolvedTypes).

Because the exemplar is read untyped, the server takes the
deserialize(signature, null, null) path, where the
SerializedTypeViolationException check (guarded by expectedType != null) does
not run. An RPC message can therefore substitute an allow-listed non-enum
type for the exemplar, and new EnumMap(nonEnumClass) throws
NullPointerException: Cannot read the array length because "this.keyUniverse" is null, which is not a SerializationException and is not caught, so the
call fails with an unexpected server error (HTTP 500) instead of a clean
deserialization failure.

The ServerCustomFieldSerializer javadoc states this is the serializer's own
responsibility:

It is this method's responsibility to verify the types of objects that it
reads. ... In practice, any call to ServerSerializationStreamReader.readObject()
should use the type checking version, passing in the expected type of the
object to be read. See the built-in GWT server custom field serializers for
examples.

Reproduction

Any RPC service with an EnumMap parameter is affected. With a service method
void store(EnumMap settings) and a serialization policy that allow-lists the
service's ordinary types (EnumMap, its enum key type, Integer, String), an
unauthenticated POST whose EnumMap exemplar is an allow-listed Integer instead
of an enum constant returns HTTP 500, and the server log shows the uncaught
NullPointerException. With the exemplar read through the type-checking path the
same request is rejected cleanly.

The existing type-check test family
(RPCTypeCheckTest / RPCTypeCheckCollectionsTest / RPCTypeCheckArraysTest)
covers every other collection serializer for exactly this substitution
(testMapHashSetSpoofingMap, testVectorHashSetSpoofingVector, and so on) but
has no case for EnumMap.

I have a fix that mirrors TreeMap/TreeSet plus a test in that family; I will
open a PR referencing this issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions