| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more contributor license |
| * agreements. See the NOTICE file distributed with this work for additional information regarding |
| * copyright ownership. The ASF licenses this file to You 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 org.apache.geode.internal; |
| |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.lang.reflect.InvocationHandler; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Proxy; |
| import java.net.URL; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| import org.apache.logging.log4j.Logger; |
| |
| import org.apache.geode.GemFireConfigException; |
| import org.apache.geode.InternalGemFireError; |
| import org.apache.geode.InternalGemFireException; |
| import org.apache.geode.distributed.internal.DistributedSystemService; |
| import org.apache.geode.logging.internal.log4j.api.LogService; |
| |
| |
| /** |
| * ObjectInputStreamFilterWrapper isolates InternalDataSerializer from the JDK's |
| * ObjectInputFilter class, which is absent in builds of java8 prior to 121 and |
| * is sun.misc.ObjectInputFilter in later builds but is java.io.ObjectInputFilter |
| * in java9. |
| * <p> |
| * This class uses reflection and dynamic proxies to find the class and create a |
| * serialization filter. Once java8 is retired and no longer supported by Geode the |
| * use of reflection should be removed ObjectInputFilter can be directly used by |
| * InternalDataSerializer. |
| */ |
| public class ObjectInputStreamFilterWrapper implements InputStreamFilter { |
| private static final Logger logger = LogService.getLogger(); |
| |
| private boolean usingJava8; |
| private Object serializationFilter; |
| |
| private Class configClass; // ObjectInputFilter$Config |
| private Method setObjectInputFilterMethod; // method on ObjectInputFilter$Config or |
| // ObjectInputStream |
| private Method createFilterMethod; // method on ObjectInputFilter$Config |
| |
| private Class filterClass; // ObjectInputFilter |
| private Object ALLOWED; // field on ObjectInputFilter |
| private Object REJECTED; // field on ObjectInputFilter |
| private Method checkInputMethod; // method on ObjectInputFilter |
| |
| private Class filterInfoClass; // ObjectInputFilter$FilterInfo |
| private Method serialClassMethod; // method on ObjectInputFilter$FilterInfo |
| |
| public ObjectInputStreamFilterWrapper(String serializationFilterSpec, |
| Collection<DistributedSystemService> services) { |
| |
| Set<String> sanctionedClasses = new HashSet<>(500); |
| for (DistributedSystemService service : services) { |
| try { |
| Collection<String> classNames = service.getSerializationAcceptlist(); |
| logger.info("loaded {} sanctioned serializables from {}", classNames.size(), |
| service.getClass().getSimpleName()); |
| sanctionedClasses.addAll(classNames); |
| } catch (IOException e) { |
| throw new InternalGemFireException("error initializing serialization filter for " + service, |
| e); |
| } |
| } |
| |
| try { |
| URL sanctionedSerializables = ClassPathLoader.getLatest() |
| .getResource(InternalDataSerializer.class, "sanctioned-geode-core-serializables.txt"); |
| Collection<String> coreClassNames = |
| InternalDataSerializer.loadClassNames(sanctionedSerializables); |
| |
| URL sanctionedManagementSerializables = ClassPathLoader.getLatest() |
| .getResource(InternalDataSerializer.class, |
| "sanctioned-geode-management-serializables.txt"); |
| Collection<String> managmentClassNames = |
| InternalDataSerializer.loadClassNames(sanctionedManagementSerializables); |
| |
| sanctionedClasses.addAll(coreClassNames); |
| sanctionedClasses.addAll(managmentClassNames); |
| } catch (IOException e) { |
| throw new InternalGemFireException( |
| "unable to read sanctionedSerializables.txt to form a serialization acceptlist", e); |
| } |
| |
| logger.info("setting a serialization filter containing {}", serializationFilterSpec); |
| |
| // try java8 - this will not throw an exception if ObjectInputFilter can't be found |
| if (createJava8Filter(serializationFilterSpec, sanctionedClasses)) { |
| return; |
| } |
| |
| // try java9 - this throws an exception if it fails to create the filter |
| createJava9Filter(serializationFilterSpec, sanctionedClasses); |
| } |
| |
| @Override |
| public void setFilterOn(ObjectInputStream objectInputStream) { |
| try { |
| if (usingJava8) { |
| setObjectInputFilterMethod.invoke(configClass, objectInputStream, serializationFilter); |
| } else { |
| setObjectInputFilterMethod.invoke(objectInputStream, serializationFilter); |
| } |
| } catch (IllegalAccessException | InvocationTargetException e) { |
| throw new InternalGemFireError("Unable to filter serialization", e); |
| } |
| } |
| |
| /** |
| * java8 has sun.misc.ObjectInputFilter and uses ObjectInputFilter$Config.setObjectInputFilter() |
| */ |
| private boolean createJava8Filter(String serializationFilterSpec, |
| Collection<String> sanctionedClasses) { |
| ClassPathLoader classPathLoader = ClassPathLoader.getLatest(); |
| try { |
| |
| filterInfoClass = classPathLoader.forName("sun.misc.ObjectInputFilter$FilterInfo"); |
| serialClassMethod = filterInfoClass.getDeclaredMethod("serialClass"); |
| |
| filterClass = classPathLoader.forName("sun.misc.ObjectInputFilter"); |
| checkInputMethod = filterClass.getDeclaredMethod("checkInput", filterInfoClass); |
| |
| Class statusClass = classPathLoader.forName("sun.misc.ObjectInputFilter$Status"); |
| ALLOWED = statusClass.getEnumConstants()[1]; |
| REJECTED = statusClass.getEnumConstants()[2]; |
| if (!ALLOWED.toString().equals("ALLOWED") || !REJECTED.toString().equals("REJECTED")) { |
| throw new GemFireConfigException( |
| "ObjectInputFilter$Status enumeration in this JDK is not as expected"); |
| } |
| |
| configClass = classPathLoader.forName("sun.misc.ObjectInputFilter$Config"); |
| setObjectInputFilterMethod = configClass.getDeclaredMethod("setObjectInputFilter", |
| ObjectInputStream.class, filterClass); |
| createFilterMethod = configClass.getDeclaredMethod("createFilter", String.class); |
| |
| serializationFilter = createSerializationFilter(serializationFilterSpec, sanctionedClasses); |
| usingJava8 = true; |
| |
| } catch (ClassNotFoundException | InvocationTargetException | IllegalAccessException |
| | NoSuchMethodException e) { |
| if (filterInfoClass != null) { |
| throw new GemFireConfigException( |
| "A serialization filter has been specified but Geode was unable to configure a filter", |
| e); |
| } |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** java9 has java.io.ObjectInputFilter and uses ObjectInputStream.setObjectInputFilter() */ |
| private void createJava9Filter(String serializationFilterSpec, |
| Collection<String> sanctionedClasses) { |
| try { |
| ClassPathLoader classPathLoader = ClassPathLoader.getLatest(); |
| |
| filterInfoClass = classPathLoader.forName("java.io.ObjectInputFilter$FilterInfo"); |
| serialClassMethod = filterInfoClass.getDeclaredMethod("serialClass"); |
| |
| |
| filterClass = classPathLoader.forName("java.io.ObjectInputFilter"); |
| checkInputMethod = filterClass.getDeclaredMethod("checkInput", filterInfoClass); |
| |
| Class statusClass = classPathLoader.forName("java.io.ObjectInputFilter$Status"); |
| ALLOWED = statusClass.getEnumConstants()[1]; |
| REJECTED = statusClass.getEnumConstants()[2]; |
| if (!ALLOWED.toString().equals("ALLOWED") || !REJECTED.toString().equals("REJECTED")) { |
| throw new GemFireConfigException( |
| "ObjectInputFilter$Status enumeration in this JDK is not as expected"); |
| } |
| |
| configClass = classPathLoader.forName("java.io.ObjectInputFilter$Config"); |
| setObjectInputFilterMethod = |
| ObjectInputStream.class.getDeclaredMethod("setObjectInputFilter", filterClass); |
| createFilterMethod = configClass.getDeclaredMethod("createFilter", String.class); |
| |
| serializationFilter = createSerializationFilter(serializationFilterSpec, sanctionedClasses); |
| |
| } catch (ClassNotFoundException | InvocationTargetException | IllegalAccessException |
| | NoSuchMethodException e) { |
| throw new GemFireConfigException( |
| "A serialization filter has been specified but Geode was unable to configure a filter", |
| e); |
| } |
| } |
| |
| |
| private Object createSerializationFilter(String serializationFilterSpec, |
| Collection<String> sanctionedClasses) |
| throws InvocationTargetException, IllegalAccessException { |
| |
| /* |
| * create a user filter with the serialization acceptlist/denylist. This will be wrapped |
| * by a filter that accept-lists sanctioned classes |
| */ |
| Object userFilter = createFilterMethod.invoke(null, serializationFilterSpec); |
| |
| InvocationHandler handler = (proxy, method, args) -> { |
| switch (method.getName()) { |
| case "checkInput": |
| Object filterInfo = args[0]; |
| Class serialClass = (Class) serialClassMethod.invoke(filterInfo); |
| if (serialClass == null) { // no class to check, so nothing to accept-list |
| return checkInputMethod.invoke(userFilter, filterInfo); |
| } |
| String className = serialClass.getName(); |
| if (serialClass.isArray()) { |
| className = serialClass.getComponentType().getName(); |
| } |
| if (sanctionedClasses.contains(className)) { |
| return ALLOWED; |
| } else { |
| Object status = checkInputMethod.invoke(userFilter, filterInfo); |
| if (status == REJECTED) { |
| logger.fatal("Serialization filter is rejecting class {}", className, |
| new Exception("")); |
| } |
| return status; |
| } |
| default: |
| throw new UnsupportedOperationException( |
| "ObjectInputFilter." + method.getName() + " is not implemented"); |
| } |
| }; |
| |
| ClassPathLoader classPathLoader = ClassPathLoader.getLatest(); |
| return Proxy.newProxyInstance(classPathLoader.asClassLoader(), new Class[] {filterClass}, |
| handler); |
| |
| } |
| |
| |
| } |