Skip to content

Commit

Permalink
Add support for reflection access filter (#1905)
Browse files Browse the repository at this point in the history
* Add support for reflection access filter

* Improve documentation

* Fix compilation errors

* Relax handling for BLOCK_ALL when invoking default constructor

* Improve handling for inherited fields

* Fix accessible test failing for static fields

* Simplify ReflectiveTypeAdapterFactory field writing

* Fix GsonBuilder changes affecting created Gson instances

* Improve documentation

* Improve handling for IllegalAccessException

For Java < 9, AccessibleObject.canAccess is not available and therefore checks
might pass even if object is not accessible, causing IllegalAccessException
later.

* Fix incorrect GsonBuilder.addReflectionAccessFilter documentation
  • Loading branch information
Marcono1234 committed Apr 17, 2022
1 parent f79ea20 commit e82637c
Show file tree
Hide file tree
Showing 13 changed files with 1,034 additions and 135 deletions.
Expand Up @@ -20,6 +20,7 @@
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import com.google.gson.JsonElement;
import com.google.gson.ReflectionAccessFilter;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.ConstructorConstructor;
Expand All @@ -30,6 +31,7 @@
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedList;
Expand All @@ -47,7 +49,7 @@ public final class GraphAdapterBuilder {

public GraphAdapterBuilder() {
this.instanceCreators = new HashMap<Type, InstanceCreator<?>>();
this.constructorConstructor = new ConstructorConstructor(instanceCreators, true);
this.constructorConstructor = new ConstructorConstructor(instanceCreators, true, Collections.<ReflectionAccessFilter>emptyList());
}
public GraphAdapterBuilder addType(Type type) {
final ObjectConstructor<?> objectConstructor = constructorConstructor.get(TypeToken.get(type));
Expand Down
12 changes: 8 additions & 4 deletions gson/src/main/java/com/google/gson/Gson.java
Expand Up @@ -156,6 +156,7 @@ public final class Gson {
final List<TypeAdapterFactory> builderHierarchyFactories;
final ToNumberStrategy objectToNumberStrategy;
final ToNumberStrategy numberToNumberStrategy;
final List<ReflectionAccessFilter> reflectionFilters;

/**
* Constructs a Gson object with default configuration. The default configuration has the
Expand Down Expand Up @@ -199,7 +200,8 @@ public Gson() {
DEFAULT_USE_JDK_UNSAFE,
LongSerializationPolicy.DEFAULT, DEFAULT_DATE_PATTERN, DateFormat.DEFAULT, DateFormat.DEFAULT,
Collections.<TypeAdapterFactory>emptyList(), Collections.<TypeAdapterFactory>emptyList(),
Collections.<TypeAdapterFactory>emptyList(), DEFAULT_OBJECT_TO_NUMBER_STRATEGY, DEFAULT_NUMBER_TO_NUMBER_STRATEGY);
Collections.<TypeAdapterFactory>emptyList(), DEFAULT_OBJECT_TO_NUMBER_STRATEGY, DEFAULT_NUMBER_TO_NUMBER_STRATEGY,
Collections.<ReflectionAccessFilter>emptyList());
}

Gson(Excluder excluder, FieldNamingStrategy fieldNamingStrategy,
Expand All @@ -211,11 +213,12 @@ public Gson() {
int timeStyle, List<TypeAdapterFactory> builderFactories,
List<TypeAdapterFactory> builderHierarchyFactories,
List<TypeAdapterFactory> factoriesToBeAdded,
ToNumberStrategy objectToNumberStrategy, ToNumberStrategy numberToNumberStrategy) {
ToNumberStrategy objectToNumberStrategy, ToNumberStrategy numberToNumberStrategy,
List<ReflectionAccessFilter> reflectionFilters) {
this.excluder = excluder;
this.fieldNamingStrategy = fieldNamingStrategy;
this.instanceCreators = instanceCreators;
this.constructorConstructor = new ConstructorConstructor(instanceCreators, useJdkUnsafe);
this.constructorConstructor = new ConstructorConstructor(instanceCreators, useJdkUnsafe, reflectionFilters);
this.serializeNulls = serializeNulls;
this.complexMapKeySerialization = complexMapKeySerialization;
this.generateNonExecutableJson = generateNonExecutableGson;
Expand All @@ -232,6 +235,7 @@ public Gson() {
this.builderHierarchyFactories = builderHierarchyFactories;
this.objectToNumberStrategy = objectToNumberStrategy;
this.numberToNumberStrategy = numberToNumberStrategy;
this.reflectionFilters = reflectionFilters;

List<TypeAdapterFactory> factories = new ArrayList<TypeAdapterFactory>();

Expand Down Expand Up @@ -296,7 +300,7 @@ public Gson() {
factories.add(jsonAdapterFactory);
factories.add(TypeAdapters.ENUM_FACTORY);
factories.add(new ReflectiveTypeAdapterFactory(
constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory));
constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory, reflectionFilters));

this.factories = Collections.unmodifiableList(factories);
}
Expand Down
32 changes: 29 additions & 3 deletions gson/src/main/java/com/google/gson/GsonBuilder.java
Expand Up @@ -22,6 +22,7 @@
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -101,6 +102,7 @@ public final class GsonBuilder {
private boolean useJdkUnsafe = DEFAULT_USE_JDK_UNSAFE;
private ToNumberStrategy objectToNumberStrategy = DEFAULT_OBJECT_TO_NUMBER_STRATEGY;
private ToNumberStrategy numberToNumberStrategy = DEFAULT_NUMBER_TO_NUMBER_STRATEGY;
private final LinkedList<ReflectionAccessFilter> reflectionFilters = new LinkedList<ReflectionAccessFilter>();

/**
* Creates a GsonBuilder instance that can be used to build Gson with various configuration
Expand Down Expand Up @@ -137,6 +139,7 @@ public GsonBuilder() {
this.useJdkUnsafe = gson.useJdkUnsafe;
this.objectToNumberStrategy = gson.objectToNumberStrategy;
this.numberToNumberStrategy = gson.numberToNumberStrategy;
this.reflectionFilters.addAll(gson.reflectionFilters);
}

/**
Expand Down Expand Up @@ -632,6 +635,28 @@ public GsonBuilder disableJdkUnsafe() {
return this;
}

/**
* Adds a reflection access filter. A reflection access filter prevents Gson from using
* reflection for the serialization and deserialization of certain classes. The logic in
* the filter specifies which classes those are.
*
* <p>Filters will be invoked in reverse registration order, that is, the most recently
* added filter will be invoked first.
*
* <p>By default Gson has no filters configured and will try to use reflection for
* all classes for which no {@link TypeAdapter} has been registered, and for which no
* built-in Gson {@code TypeAdapter} exists.
*
* @param filter filter to add
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
*/
public GsonBuilder addReflectionAccessFilter(ReflectionAccessFilter filter) {
if (filter == null) throw new NullPointerException();

reflectionFilters.addFirst(filter);
return this;
}

/**
* Creates a {@link Gson} instance based on the current configuration. This method is free of
* side-effects to this {@code GsonBuilder} instance and hence can be called multiple times.
Expand All @@ -649,12 +674,13 @@ public Gson create() {

addTypeAdaptersForDate(datePattern, dateStyle, timeStyle, factories);

return new Gson(excluder, fieldNamingPolicy, instanceCreators,
return new Gson(excluder, fieldNamingPolicy, new HashMap<Type, InstanceCreator<?>>(instanceCreators),
serializeNulls, complexMapKeySerialization,
generateNonExecutableJson, escapeHtmlChars, prettyPrinting, lenient,
serializeSpecialFloatingPointValues, useJdkUnsafe, longSerializationPolicy,
datePattern, dateStyle, timeStyle,
this.factories, this.hierarchyFactories, factories, objectToNumberStrategy, numberToNumberStrategy);
datePattern, dateStyle, timeStyle, new ArrayList<TypeAdapterFactory>(this.factories),
new ArrayList<TypeAdapterFactory>(this.hierarchyFactories), factories,
objectToNumberStrategy, numberToNumberStrategy, new ArrayList<ReflectionAccessFilter>(reflectionFilters));
}

private void addTypeAdaptersForDate(String datePattern, int dateStyle, int timeStyle,
Expand Down
194 changes: 194 additions & 0 deletions gson/src/main/java/com/google/gson/ReflectionAccessFilter.java
@@ -0,0 +1,194 @@
package com.google.gson;

import java.lang.reflect.AccessibleObject;

import com.google.gson.internal.ReflectionAccessFilterHelper;

/**
* Filter for determining whether reflection based serialization and
* deserialization is allowed for a class.
*
* <p>A filter can be useful in multiple scenarios, for example when
* upgrading to newer Java versions which use the Java Platform Module
* System (JPMS). A filter then allows to {@linkplain FilterResult#BLOCK_INACCESSIBLE
* prevent making inaccessible members accessible}, even if the used
* Java version might still allow illegal access (but logs a warning),
* or if {@code java} command line arguments are used to open the inaccessible
* packages to other parts of the application. This interface defines some
* convenience filters for this task, such as {@link #BLOCK_INACCESSIBLE_JAVA}.
*
* <p>A filter can also be useful to prevent mixing model classes of a
* project with other non-model classes; the filter could
* {@linkplain FilterResult#BLOCK_ALL block all reflective access} to
* non-model classes.
*
* <p>A reflection access filter is similar to an {@link ExclusionStrategy}
* with the major difference that a filter will cause an exception to be
* thrown when access is disallowed while an exclusion strategy just skips
* fields and classes.
*
* @see GsonBuilder#addReflectionAccessFilter(ReflectionAccessFilter)
*/
public interface ReflectionAccessFilter {
/**
* Result of a filter check.
*/
enum FilterResult {
/**
* Reflection access for the class is allowed.
*
* <p>Note that this does not affect the Java access checks in any way,
* it only permits Gson to try using reflection for a class. The Java
* runtime might still deny such access.
*/
ALLOW,
/**
* The filter is indecisive whether reflection access should be allowed.
* The next registered filter will be consulted to get the result. If
* there is no next filter, this result acts like {@link #ALLOW}.
*/
INDECISIVE,
/**
* Blocks reflection access if a member of the class is not accessible
* by default and would have to be made accessible. This is unaffected
* by any {@code java} command line arguments being used to make packages
* accessible, or by module declaration directives which <i>open</i> the
* complete module or certain packages for reflection and will consider
* such packages inaccessible.
*
* <p>Note that this <b>only works for Java 9 and higher</b>, for older
* Java versions its functionality will be limited and it might behave like
* {@link #ALLOW}. Access checks are only performed as defined by the Java
* Language Specification (<a href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-6.html#jls-6.6">JLS 11 &sect;6.6</a>),
* restrictions imposed by a {@link SecurityManager} are not considered.
*
* <p>This result type is mainly intended to help enforce the access checks of
* the Java Platform Module System. It allows detecting illegal access, even if
* the used Java version would only log a warning, or is configured to open
* packages for reflection using command line arguments.
*
* @see AccessibleObject#canAccess(Object)
*/
BLOCK_INACCESSIBLE,
/**
* Blocks all reflection access for the class. Other means for serializing
* and deserializing the class, such as a {@link TypeAdapter}, have to
* be used.
*/
BLOCK_ALL
}

/**
* Blocks all reflection access to members of standard Java classes which are
* not accessible by default. However, reflection access is still allowed for
* classes for which all fields are accessible and which have an accessible
* no-args constructor (or for which an {@link InstanceCreator} has been registered).
*
* <p>If this filter encounters a class other than a standard Java class it
* returns {@link FilterResult#INDECISIVE}.
*
* <p>This filter is mainly intended to help enforcing the access checks of
* Java Platform Module System. It allows detecting illegal access, even if
* the used Java version would only log a warning, or is configured to open
* packages for reflection. However, this filter <b>only works for Java 9 and
* higher</b>, when using an older Java version its functionality will be
* limited.
*
* <p>Note that this filter might not cover all standard Java classes. Currently
* only classes in a {@code java.*} or {@code javax.*} package are considered. The
* set of detected classes might be expanded in the future without prior notice.
*
* @see FilterResult#BLOCK_INACCESSIBLE
*/
ReflectionAccessFilter BLOCK_INACCESSIBLE_JAVA = new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return ReflectionAccessFilterHelper.isJavaType(rawClass)
? FilterResult.BLOCK_INACCESSIBLE
: FilterResult.INDECISIVE;
}
};

/**
* Blocks all reflection access to members of standard Java classes.
*
* <p>If this filter encounters a class other than a standard Java class it
* returns {@link FilterResult#INDECISIVE}.
*
* <p>This filter is mainly intended to prevent depending on implementation
* details of the Java platform and to help applications prepare for upgrading
* to the Java Platform Module System.
*
* <p>Note that this filter might not cover all standard Java classes. Currently
* only classes in a {@code java.*} or {@code javax.*} package are considered. The
* set of detected classes might be expanded in the future without prior notice.
*
* @see #BLOCK_INACCESSIBLE_JAVA
* @see FilterResult#BLOCK_ALL
*/
ReflectionAccessFilter BLOCK_ALL_JAVA = new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return ReflectionAccessFilterHelper.isJavaType(rawClass)
? FilterResult.BLOCK_ALL
: FilterResult.INDECISIVE;
}
};

/**
* Blocks all reflection access to members of standard Android classes.
*
* <p>If this filter encounters a class other than a standard Android class it
* returns {@link FilterResult#INDECISIVE}.
*
* <p>This filter is mainly intended to prevent depending on implementation
* details of the Android platform.
*
* <p>Note that this filter might not cover all standard Android classes. Currently
* only classes in an {@code android.*} or {@code androidx.*} package, and standard
* Java classes in a {@code java.*} or {@code javax.*} package are considered. The
* set of detected classes might be expanded in the future without prior notice.
*
* @see FilterResult#BLOCK_ALL
*/
ReflectionAccessFilter BLOCK_ALL_ANDROID = new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return ReflectionAccessFilterHelper.isAndroidType(rawClass)
? FilterResult.BLOCK_ALL
: FilterResult.INDECISIVE;
}
};

/**
* Blocks all reflection access to members of classes belonging to programming
* language platforms, such as Java, Android, Kotlin or Scala.
*
* <p>If this filter encounters a class other than a standard platform class it
* returns {@link FilterResult#INDECISIVE}.
*
* <p>This filter is mainly intended to prevent depending on implementation
* details of the platform classes.
*
* <p>Note that this filter might not cover all platform classes. Currently it
* combines the filters {@link #BLOCK_ALL_JAVA} and {@link #BLOCK_ALL_ANDROID},
* and checks for other language-specific platform classes like {@code kotlin.*}.
* The set of detected classes might be expanded in the future without prior notice.
*
* @see FilterResult#BLOCK_ALL
*/
ReflectionAccessFilter BLOCK_ALL_PLATFORM = new ReflectionAccessFilter() {
@Override public FilterResult check(Class<?> rawClass) {
return ReflectionAccessFilterHelper.isAnyPlatformType(rawClass)
? FilterResult.BLOCK_ALL
: FilterResult.INDECISIVE;
}
};

/**
* Checks if reflection access should be allowed for a class.
*
* @param rawClass
* Class to check
* @return
* Result indicating whether reflection access is allowed
*/
FilterResult check(Class<?> rawClass);
}

0 comments on commit e82637c

Please sign in to comment.