Back to Repositories

Validating Reflection Access Control Mechanisms in google/gson

This test suite validates the ReflectionAccessFilter functionality in Google’s Gson library, focusing on controlling reflection-based access during JSON serialization and deserialization. It ensures proper handling of access restrictions for Java classes, constructors, and fields while maintaining security and configurability.

Test Coverage Overview

The test suite provides comprehensive coverage of ReflectionAccessFilter functionality, including:
  • Blocking inaccessible Java fields and classes
  • Handling JDK class extensions and inheritance
  • Constructor accessibility validation
  • Custom type adapter integration
  • Collection interface handling
Key edge cases include private fields, inaccessible constructors, and interface deserialization scenarios.

Implementation Analysis

The testing approach employs JUnit framework with systematic validation of filter behaviors. It uses a combination of custom filter implementations and predefined constants to verify access control mechanisms.
  • Custom ReflectionAccessFilter implementations
  • Assertion-based validation using Truth framework
  • Exception handling verification
  • Type adapter and instance creator integration tests

Technical Details

Testing infrastructure includes:
  • JUnit 4 test framework
  • Google Truth assertion library
  • Custom test classes and inheritance hierarchies
  • Mock objects and test fixtures
  • Exception verification utilities

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through:
  • Comprehensive edge case coverage
  • Clear test method naming and organization
  • Thorough assertion validation
  • Proper test isolation and setup
  • Effective use of JUnit features and assertions

google/gson

gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java

            
/*
 * Copyright (C) 2022 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.gson.functional;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.ReflectionAccessFilter;
import com.google.gson.ReflectionAccessFilter.FilterResult;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.junit.AssumptionViolatedException;
import org.junit.Test;

public class ReflectionAccessFilterTest {
  // Reader has protected `lock` field which cannot be accessed
  private static class ClassExtendingJdkClass extends Reader {
    @Override
    public int read(char[] cbuf, int off, int len) throws IOException {
      return 0;
    }

    @Override
    public void close() throws IOException {}
  }

  @Test
  public void testBlockInaccessibleJava() {
    Gson gson =
        new GsonBuilder()
            .addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA)
            .create();

    // Serialization should fail for classes with non-public fields
    // Note: This test is rather brittle and depends on the JDK implementation
    var e =
        assertThrows(
            "Expected exception; test needs to be run with Java >= 9",
            JsonIOException.class,
            () -> gson.toJson(new File("a")));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Field 'java.io.File#path' is not accessible and ReflectionAccessFilter does not"
                + " permit making it accessible. Register a TypeAdapter for the declaring type,"
                + " adjust the access filter or increase the visibility of the element and its"
                + " declaring type.");
  }

  @Test
  public void testDontBlockAccessibleJava() throws ReflectiveOperationException {
    Gson gson =
        new GsonBuilder()
            .addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA)
            .create();

    // Serialization should succeed for classes with only public fields.
    // Not many JDK classes have mutable public fields, thank goodness, but java.awt.Point does.
    Class<?> pointClass;
    try {
      pointClass = Class.forName("java.awt.Point");
    } catch (ClassNotFoundException e) {
      // If not found then we don't have AWT and the rest of the test can be skipped.
      throw new AssumptionViolatedException("java.awt.Point not present", e);
    }
    Constructor<?> pointConstructor = pointClass.getConstructor(int.class, int.class);
    Object point = pointConstructor.newInstance(1, 2);
    String json = gson.toJson(point);
    assertThat(json).isEqualTo("{\"x\":1,\"y\":2}");
  }

  @Test
  public void testBlockInaccessibleJavaExtendingJdkClass() {
    Gson gson =
        new GsonBuilder()
            .addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA)
            .create();

    var e =
        assertThrows(
            "Expected exception; test needs to be run with Java >= 9",
            JsonIOException.class,
            () -> gson.toJson(new ClassExtendingJdkClass()));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Field 'java.io.Reader#lock' is not accessible and ReflectionAccessFilter does not"
                + " permit making it accessible. Register a TypeAdapter for the declaring type,"
                + " adjust the access filter or increase the visibility of the element and its"
                + " declaring type.");
  }

  @Test
  public void testBlockAllJava() {
    Gson gson =
        new GsonBuilder().addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_ALL_JAVA).create();

    // Serialization should fail for any Java class without custom adapter
    var e = assertThrows(JsonIOException.class, () -> gson.toJson(Thread.currentThread()));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "ReflectionAccessFilter does not permit using reflection for class java.lang.Thread."
                + " Register a TypeAdapter for this type or adjust the access filter.");
  }

  @Test
  public void testBlockAllJavaExtendingJdkClass() {
    Gson gson =
        new GsonBuilder().addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_ALL_JAVA).create();

    var e = assertThrows(JsonIOException.class, () -> gson.toJson(new ClassExtendingJdkClass()));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "ReflectionAccessFilter does not permit using reflection for class java.io.Reader"
                + " (supertype of class"
                + " com.google.gson.functional.ReflectionAccessFilterTest$ClassExtendingJdkClass)."
                + " Register a TypeAdapter for this type or adjust the access filter.");
  }

  private static class ClassWithStaticField {
    @SuppressWarnings({"unused", "NonFinalStaticField"})
    private static int i = 1;
  }

  @Test
  public void testBlockInaccessibleStaticField() {
    Gson gson =
        new GsonBuilder()
            .addReflectionAccessFilter(
                new ReflectionAccessFilter() {
                  @Override
                  public FilterResult check(Class<?> rawClass) {
                    return FilterResult.BLOCK_INACCESSIBLE;
                  }
                })
            // Include static fields
            .excludeFieldsWithModifiers(0)
            .create();

    var e =
        assertThrows(
            "Expected exception; test needs to be run with Java >= 9",
            JsonIOException.class,
            () -> gson.toJson(new ClassWithStaticField()));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Field 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithStaticField#i'"
                + " is not accessible and ReflectionAccessFilter does not permit making it"
                + " accessible. Register a TypeAdapter for the declaring type, adjust the access"
                + " filter or increase the visibility of the element and its declaring type.");
  }

  private static class SuperTestClass {}

  private static class SubTestClass extends SuperTestClass {
    @SuppressWarnings("unused")
    public int i = 1;
  }

  private static class OtherClass {
    @SuppressWarnings("unused")
    public int i = 2;
  }

  @Test
  public void testDelegation() {
    Gson gson =
        new GsonBuilder()
            .addReflectionAccessFilter(
                new ReflectionAccessFilter() {
                  @Override
                  public FilterResult check(Class<?> rawClass) {
                    // INDECISIVE in last filter should act like ALLOW
                    return SuperTestClass.class.isAssignableFrom(rawClass)
                        ? FilterResult.BLOCK_ALL
                        : FilterResult.INDECISIVE;
                  }
                })
            .addReflectionAccessFilter(
                new ReflectionAccessFilter() {
                  @Override
                  public FilterResult check(Class<?> rawClass) {
                    // INDECISIVE should delegate to previous filter
                    return rawClass == SubTestClass.class
                        ? FilterResult.ALLOW
                        : FilterResult.INDECISIVE;
                  }
                })
            .create();

    // Filter disallows SuperTestClass
    var e = assertThrows(JsonIOException.class, () -> gson.toJson(new SuperTestClass()));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "ReflectionAccessFilter does not permit using reflection for class"
                + " com.google.gson.functional.ReflectionAccessFilterTest$SuperTestClass."
                + " Register a TypeAdapter for this type or adjust the access filter.");

    // But registration order is reversed, so filter for SubTestClass allows reflection
    String json = gson.toJson(new SubTestClass());
    assertThat(json).isEqualTo("{\"i\":1}");

    // And unrelated class should not be affected
    json = gson.toJson(new OtherClass());
    assertThat(json).isEqualTo("{\"i\":2}");
  }

  private static class ClassWithPrivateField {
    @SuppressWarnings("unused")
    private int i = 1;
  }

  private static class ExtendingClassWithPrivateField extends ClassWithPrivateField {}

  @Test
  public void testAllowForSupertype() {
    Gson gson =
        new GsonBuilder()
            .addReflectionAccessFilter(
                new ReflectionAccessFilter() {
                  @Override
                  public FilterResult check(Class<?> rawClass) {
                    return FilterResult.BLOCK_INACCESSIBLE;
                  }
                })
            .create();

    // First make sure test is implemented correctly and access is blocked
    var e =
        assertThrows(
            "Expected exception; test needs to be run with Java >= 9",
            JsonIOException.class,
            () -> gson.toJson(new ExtendingClassWithPrivateField()));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Field"
                + " 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateField#i'"
                + " is not accessible and ReflectionAccessFilter does not permit making it"
                + " accessible. Register a TypeAdapter for the declaring type, adjust the access"
                + " filter or increase the visibility of the element and its declaring type.");

    Gson gson2 =
        gson.newBuilder()
            // Allow reflective access for supertype
            .addReflectionAccessFilter(
                new ReflectionAccessFilter() {
                  @Override
                  public FilterResult check(Class<?> rawClass) {
                    return rawClass == ClassWithPrivateField.class
                        ? FilterResult.ALLOW
                        : FilterResult.INDECISIVE;
                  }
                })
            .create();

    // Inherited (inaccessible) private field should have been made accessible
    String json = gson2.toJson(new ExtendingClassWithPrivateField());
    assertThat(json).isEqualTo("{\"i\":1}");
  }

  private static class ClassWithPrivateNoArgsConstructor {
    private ClassWithPrivateNoArgsConstructor() {}
  }

  @Test
  public void testInaccessibleNoArgsConstructor() {
    Gson gson =
        new GsonBuilder()
            .addReflectionAccessFilter(
                new ReflectionAccessFilter() {
                  @Override
                  public FilterResult check(Class<?> rawClass) {
                    return FilterResult.BLOCK_INACCESSIBLE;
                  }
                })
            .create();

    var e =
        assertThrows(
            "Expected exception; test needs to be run with Java >= 9",
            JsonIOException.class,
            () -> gson.fromJson("{}", ClassWithPrivateNoArgsConstructor.class));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Unable to invoke no-args constructor of class"
                + " com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateNoArgsConstructor;"
                + " constructor is not accessible and ReflectionAccessFilter does not permit making"
                + " it accessible. Register an InstanceCreator or a TypeAdapter for this type,"
                + " change the visibility of the constructor or adjust the access filter.");
  }

  private static class ClassWithoutNoArgsConstructor {
    public String s;

    public ClassWithoutNoArgsConstructor(String s) {
      this.s = s;
    }
  }

  @Test
  public void testClassWithoutNoArgsConstructor() {
    GsonBuilder gsonBuilder =
        new GsonBuilder()
            .addReflectionAccessFilter(
                new ReflectionAccessFilter() {
                  @Override
                  public FilterResult check(Class<?> rawClass) {
                    // Even BLOCK_INACCESSIBLE should prevent usage of Unsafe for object creation
                    return FilterResult.BLOCK_INACCESSIBLE;
                  }
                });
    Gson gson = gsonBuilder.create();

    var e =
        assertThrows(
            JsonIOException.class, () -> gson.fromJson("{}", ClassWithoutNoArgsConstructor.class));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Unable to create instance of class"
                + " com.google.gson.functional.ReflectionAccessFilterTest$ClassWithoutNoArgsConstructor;"
                + " ReflectionAccessFilter does not permit using reflection or Unsafe. Register an"
                + " InstanceCreator or a TypeAdapter for this type or adjust the access filter to"
                + " allow using reflection.");

    // But should not fail when custom TypeAdapter is specified
    Gson gson2 =
        gson.newBuilder()
            .registerTypeAdapter(
                ClassWithoutNoArgsConstructor.class,
                new TypeAdapter<ClassWithoutNoArgsConstructor>() {
                  @Override
                  public ClassWithoutNoArgsConstructor read(JsonReader in) throws IOException {
                    in.skipValue();
                    return new ClassWithoutNoArgsConstructor("TypeAdapter");
                  }

                  @Override
                  public void write(JsonWriter out, ClassWithoutNoArgsConstructor value) {
                    throw new AssertionError("Not needed for test");
                  }
                })
            .create();
    ClassWithoutNoArgsConstructor deserialized =
        gson2.fromJson("{}", ClassWithoutNoArgsConstructor.class);
    assertThat(deserialized.s).isEqualTo("TypeAdapter");

    // But should not fail when custom InstanceCreator is specified
    gson2 =
        gsonBuilder
            .registerTypeAdapter(
                ClassWithoutNoArgsConstructor.class,
                new InstanceCreator<ClassWithoutNoArgsConstructor>() {
                  @Override
                  public ClassWithoutNoArgsConstructor createInstance(Type type) {
                    return new ClassWithoutNoArgsConstructor("InstanceCreator");
                  }
                })
            .create();
    deserialized = gson2.fromJson("{}", ClassWithoutNoArgsConstructor.class);
    assertThat(deserialized.s).isEqualTo("InstanceCreator");
  }

  /**
   * When using {@link FilterResult#BLOCK_ALL}, registering only a {@link JsonSerializer} but not
   * performing any deserialization should not throw any exception.
   */
  @Test
  public void testBlockAllPartial() {
    Gson gson =
        new GsonBuilder()
            .addReflectionAccessFilter(
                new ReflectionAccessFilter() {
                  @Override
                  public FilterResult check(Class<?> rawClass) {
                    return FilterResult.BLOCK_ALL;
                  }
                })
            .registerTypeAdapter(
                OtherClass.class,
                new JsonSerializer<OtherClass>() {
                  @Override
                  public JsonElement serialize(
                      OtherClass src, Type typeOfSrc, JsonSerializationContext context) {
                    return new JsonPrimitive(123);
                  }
                })
            .create();

    String json = gson.toJson(new OtherClass());
    assertThat(json).isEqualTo("123");

    // But deserialization should fail
    var e = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", OtherClass.class));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "ReflectionAccessFilter does not permit using reflection for class"
                + " com.google.gson.functional.ReflectionAccessFilterTest$OtherClass. Register a"
                + " TypeAdapter for this type or adjust the access filter.");
  }

  /**
   * Should not fail when deserializing collection interface (even though this goes through {@link
   * ConstructorConstructor} as well)
   */
  @Test
  public void testBlockAllCollectionInterface() {
    Gson gson =
        new GsonBuilder()
            .addReflectionAccessFilter(
                new ReflectionAccessFilter() {
                  @Override
                  public FilterResult check(Class<?> rawClass) {
                    return FilterResult.BLOCK_ALL;
                  }
                })
            .create();
    List<?> deserialized = gson.fromJson("[1.0]", List.class);
    assertThat(deserialized.get(0)).isEqualTo(1.0);
  }

  /**
   * Should not fail when deserializing specific collection implementation (even though this goes
   * through {@link ConstructorConstructor} as well)
   */
  @Test
  public void testBlockAllCollectionImplementation() {
    Gson gson =
        new GsonBuilder()
            .addReflectionAccessFilter(
                new ReflectionAccessFilter() {
                  @Override
                  public FilterResult check(Class<?> rawClass) {
                    return FilterResult.BLOCK_ALL;
                  }
                })
            .create();
    List<?> deserialized = gson.fromJson("[1.0]", LinkedList.class);
    assertThat(deserialized.get(0)).isEqualTo(1.0);
  }

  /**
   * When trying to deserialize interface, an exception for the missing adapter should be thrown,
   * even if {@link FilterResult#BLOCK_INACCESSIBLE} is used.
   */
  @Test
  public void testBlockInaccessibleInterface() {
    Gson gson =
        new GsonBuilder()
            .addReflectionAccessFilter(
                new ReflectionAccessFilter() {
                  @Override
                  public FilterResult check(Class<?> rawClass) {
                    return FilterResult.BLOCK_INACCESSIBLE;
                  }
                })
            .create();

    var e = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", Runnable.class));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            "Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for"
                + " this type. Interface name: java.lang.Runnable");
  }

  /**
   * Verifies that the predefined filter constants have meaningful {@code toString()} output to make
   * debugging easier.
   */
  @Test
  public void testConstantsToString() throws Exception {
    List<Field> constantFields = new ArrayList<>();

    for (Field f : ReflectionAccessFilter.class.getFields()) {
      // Only include ReflectionAccessFilter constants (in case any other fields are added in the
      // future)
      if (f.getType() == ReflectionAccessFilter.class) {
        constantFields.add(f);
      }
    }

    assertThat(constantFields).isNotEmpty();
    for (Field f : constantFields) {
      Object constant = f.get(null);
      assertThat(constant.toString()).isEqualTo("ReflectionAccessFilter#" + f.getName());
    }
  }
}