/* * Copyright (c) 2013 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.util.groovy.sandbox.internal; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Set; import java.util.UUID; import org.codehaus.groovy.runtime.GStringImpl; import org.codehaus.groovy.runtime.InvokerHelper; import org.kohsuke.groovy.sandbox.GroovyInterceptor; import eu.esdihumboldt.util.groovy.sandbox.GroovyRestrictionException; import groovy.lang.Closure; import groovy.lang.GString; import groovy.lang.MissingPropertyException; import groovy.lang.Range; import groovy.lang.Script; /** * {@link GroovyInterceptor} which allows some basic classes but is pretty * restrictive. Constructor offers parameters to allow more classes. * * @author Kai Schwierczek */ public class RestrictiveGroovyInterceptor extends GroovyInterceptor { /** * Classes, which may be initialized, and all their methods may be used. */ private static final Set<Class<?>> allowedClasses = new HashSet<>(); /** * Classes, which may be initialized, and all their methods, and method * missing/getProperty() may be used. */ private static final Set<Class<?>> allAllowedClasses = new HashSet<>(); /** * Generally disallowed methods. */ private static final Set<String> disallowedMethods = new HashSet<>(); /** * Generally disallowed properties (write access). */ private static final Set<String> disallowedWriteProperties = new HashSet<>(); /** * Disallowed Script methods. */ private static final Set<String> disallowedScriptMethods = new HashSet<>(); /** * Disallowed Script properties (write access). */ private static final Set<String> disallowedScriptWriteProperties = new HashSet<>(); /** * Disallowed Closure methods. */ private static final Set<String> disallowedClosureMethods = new HashSet<>(); /** * Disallowed Closure properties (write access). */ private static final Set<String> disallowedClosureWriteProperties = new HashSet<>(); /** * Allowed packages. */ private static final List<AllowedPrefix> allowedPackages = new ArrayList<>(); static { // standard classes allowedClasses.add(String.class); allowedClasses.add(Byte.class); allowedClasses.add(Short.class); allowedClasses.add(Integer.class); allowedClasses.add(Long.class); allowedClasses.add(Float.class); allowedClasses.add(Double.class); allowedClasses.add(BigInteger.class); allowedClasses.add(BigDecimal.class); allowedClasses.add(Math.class); allowedClasses.add(Date.class); // allowedClasses.add(Boolean.class); // helper classes allowedClasses.add(SimpleDateFormat.class); allowedClasses.add(UUID.class); // Collections & Classes used by Groovy allowedClasses.add(LinkedHashMap.class); allowedClasses.add(ArrayList.class); allowedClasses.add(Range.class); allowedClasses.add(GStringImpl.class); // Some more collections allowedClasses.add(HashMap.class); allowedClasses.add(HashSet.class); allowedClasses.add(Collections.class); // Binding classes allowedClasses.add(Timestamp.class); // Google collect allowedPackages.add(new AllowedPrefix("com.google.common.collect", true)); // general disallow access to specific Groovy methods disallowedMethods.add("invokeMethod"); disallowedMethods.add("setMetaClass"); disallowedMethods.add("setProperty"); disallowedWriteProperties.add("metaClass"); // forbid self-execution of script and overwriting of binding disallowedScriptMethods.add("run"); disallowedScriptMethods.add("evaluate"); disallowedScriptMethods.add("setBinding"); disallowedScriptWriteProperties.add("binding"); // forbid explicit setting of delegate, resolve strategy and directive disallowedClosureMethods.add("setDelegate"); disallowedClosureMethods.add("setResolveStrategy"); disallowedClosureMethods.add("setDirective"); // and rehydrate as that results basically in setting the owner, // delegate and thisObject disallowedClosureMethods.add("rehydrate"); disallowedClosureWriteProperties.add("delegate"); disallowedClosureWriteProperties.add("resolveStrategy"); disallowedClosureWriteProperties.add("directive"); } /** * AllowedPackage saves information to allow the use of all classes with a * given prefix. */ public static class AllowedPrefix { private final String prefix; private final boolean allowChildren; /** * Default constructor which accepts a package name. * * @param prefix the package prefix (final '.' is added if not present) * @param allowChildren whether child-packages are allowed, too */ public AllowedPrefix(String prefix, boolean allowChildren) { if (!prefix.endsWith(".")) prefix += '.'; this.prefix = prefix; this.allowChildren = allowChildren; } /** * Checks whether the given class is allowed to be used because of this * allowed package. * * @param clazz the class to test * @return true, if the class may be used, false otherwise */ public boolean checkAllowed(Class<?> clazz) { String className = clazz.getName(); if (className.startsWith(prefix)) { return allowChildren || !className.substring(prefix.length()).contains("."); } else { return false; } } } private final Set<Class<?>> instanceAllowedClasses = new HashSet<>(allowedClasses); private final Set<Class<?>> instanceAllAllowedClasses = new HashSet<>(allAllowedClasses); private final List<AllowedPrefix> instanceAllowedPackages = new ArrayList<>(allowedPackages); /** * Constructor using additional allowed classes. * * @param additionalAllowedClasses classes, which may be initialized, and * all their declared methods may be used * @param additionalAllAllowedClasses classes, which may be initialized, and * any call on them is allowed (has to implement methodMissing or * equal) * @param additionalAllowedPackages packages whose classes and their * declared methods may be used */ public RestrictiveGroovyInterceptor(Set<Class<?>> additionalAllowedClasses, Set<Class<?>> additionalAllAllowedClasses, List<AllowedPrefix> additionalAllowedPackages) { instanceAllowedClasses.addAll(additionalAllowedClasses); instanceAllowedClasses.addAll(additionalAllAllowedClasses); instanceAllAllowedClasses.addAll(additionalAllAllowedClasses); instanceAllowedPackages.addAll(additionalAllowedPackages); } @Override public Object onStaticCall(Invoker invoker, @SuppressWarnings("rawtypes") Class receiver, String method, Object... args) throws Throwable { if (isAllowedClass(receiver) || isScriptClass(receiver)) return super.onStaticCall(invoker, receiver, method, args); else throw new GroovyRestrictionException("using class " + receiver.getSimpleName() + " is not allowed!"); } @Override public Object onNewInstance(Invoker invoker, @SuppressWarnings("rawtypes") Class receiver, Object... args) throws Throwable { // classes defined in the script would be okay, sadly it is not possible // to identify those? if (isAllowedClass(receiver) || isScriptClass(receiver)) return super.onNewInstance(invoker, receiver, args); else throw new GroovyRestrictionException("using class " + receiver.getSimpleName() + " is not allowed!"); } @Override public Object onMethodCall(Invoker invoker, Object receiver, String method, Object... args) throws Throwable { if (disallowedMethods.contains(method)) throw new GroovyRestrictionException("using methods named " + method + " is not allowed in Groovy transformations!"); else if (receiver instanceof Closure && disallowedClosureMethods.contains(method)) throw new GroovyRestrictionException("using the closure method " + method + " is not allowed in Groovy transformations!"); // Return value doesn't matter! // true -> allowed delegation found // false -> no disallowed delegation found checkMethodCall(receiver, method); return super.onMethodCall(invoker, receiver, method, args); } private boolean checkMethodCall(Object receiver, String method) throws GroovyRestrictionException { if (receiver instanceof Closure) { // Closure method names were tested before. Closure<?> closure = (Closure<?>) receiver; Object owner = closure.getOwner(); Object delegate = closure.getDelegate(); int rs = closure.getResolveStrategy(); // Check owner first. if (rs == Closure.OWNER_FIRST || rs == Closure.OWNER_ONLY) if (checkMethodCall(owner, method)) return true; // Check delegate first/second. if (rs == Closure.OWNER_FIRST || rs == Closure.DELEGATE_FIRST || rs == Closure.DELEGATE_ONLY) if (delegate != null && delegate != closure) if (checkMethodCall(delegate, method)) return true; // Check owner second. if (rs == Closure.DELEGATE_FIRST) if (checkMethodCall(owner, method)) return true; // Cannot be 100% sure whether the call will be handled by // delegation to this closure. return false; } else if (isAllowedClass(receiver.getClass())) { checkExecute(receiver, method); return instanceAllAllowedClasses.contains(receiver.getClass()) || !InvokerHelper.getMetaClass(receiver).respondsTo(receiver, method).isEmpty(); } else if (isScriptClass(receiver.getClass()) && !disallowedScriptMethods.contains(method)) return !InvokerHelper.getMetaClass(receiver).respondsTo(receiver, method).isEmpty(); throw new GroovyRestrictionException("Possible access of method " + method + " on class " + receiver.getClass().getSimpleName() + " is not allowed in Groovy transformations!"); } /** * Checks for an execute call on List, String, String[] and GString. * * @param receiver the receiver object * @param method the method name */ private void checkExecute(Object receiver, String method) { if ("execute".equals(method)) { if (receiver instanceof List || receiver instanceof String || receiver.getClass().isArray() || receiver instanceof String[] || receiver instanceof GString) { throw new GroovyRestrictionException( "Possible access of method execute on List, String, String[] and GString is not allowed in Groovy transformations!"); } } } private boolean isScriptClass(Class<?> receiver) { // while-doesn't really do anything, because Groovy extracts classes // defined in scripts as stand-alone classes. // while (receiver.getEnclosingClass() != null) // receiver = receiver.getEnclosingClass(); return Script.class.isAssignableFrom(receiver); } @Override public Object onGetProperty(Invoker invoker, Object receiver, String property) throws Throwable { if (receiver instanceof Class<?> && isAllowedClass((Class<?>) receiver) && !"class".equals(property)) return super.onGetProperty(invoker, receiver, property); checkPropertyAccess(receiver, property, false); return super.onGetProperty(invoker, receiver, property); } @Override public Object onSetProperty(Invoker invoker, Object receiver, String property, Object value) throws Throwable { if (disallowedWriteProperties.contains(property)) throw new GroovyRestrictionException("setting the property " + property + " is not allowed in Groovy transformations!"); if (receiver instanceof Closure && disallowedClosureWriteProperties.contains(property)) throw new GroovyRestrictionException("setting the closure property " + property + " is not allowed in Groovy transformations!"); checkPropertyAccess(receiver, property, true); return super.onSetProperty(invoker, receiver, property, value); } private boolean checkPropertyAccess(Object receiver, String property, boolean set) throws GroovyRestrictionException { if (receiver instanceof Closure) { // Closure properties were tested before. Closure<?> closure = (Closure<?>) receiver; Object owner = closure.getOwner(); Object delegate = closure.getDelegate(); int rs = closure.getResolveStrategy(); // Check owner first. if (rs == Closure.OWNER_FIRST || rs == Closure.OWNER_ONLY) if (checkPropertyAccess(owner, property, set)) return true; // Check delegate first/second. if (rs == Closure.OWNER_FIRST || rs == Closure.DELEGATE_FIRST || rs == Closure.DELEGATE_ONLY) if (delegate != null && delegate != closure) if (checkPropertyAccess(delegate, property, set)) return true; // Check owner second. if (rs == Closure.DELEGATE_FIRST) if (checkPropertyAccess(owner, property, set)) return true; // Cannot be 100% sure whether the property will be handled by // delegation to this closure. return false; } else if (instanceAllAllowedClasses.contains(receiver.getClass())) return true; else if (isAllowedClass(receiver.getClass())) return hasProperty(receiver, property); else if (isScriptClass(receiver.getClass()) && (!set || !disallowedScriptWriteProperties.contains(property))) return hasProperty(receiver, property); throw new GroovyRestrictionException("Possible " + (set ? "write " : "") + "access of property " + property + " on class " + receiver.getClass().getSimpleName() + " is not allowed in Groovy transformations!"); } @Override public Object onGetAttribute(Invoker invoker, Object receiver, String attribute) throws Throwable { checkPropertyAccess(receiver, attribute, false); return super.onGetAttribute(invoker, receiver, attribute); } @Override public Object onSetAttribute(Invoker invoker, Object receiver, String attribute, Object value) throws Throwable { if (disallowedWriteProperties.contains(attribute)) throw new GroovyRestrictionException("setting the property " + attribute + " is not allowed in Groovy transformations!"); if (receiver instanceof Closure && disallowedClosureWriteProperties.contains(attribute)) throw new GroovyRestrictionException("setting the closure property " + attribute + " is not allowed in Groovy transformations!"); checkPropertyAccess(receiver, attribute, true); return super.onSetAttribute(invoker, receiver, attribute, value); } @Override public Object onGetArray(Invoker invoker, Object receiver, Object index) throws Throwable { // generally allow array access for now return super.onGetArray(invoker, receiver, index); } @Override public Object onSetArray(Invoker invoker, Object receiver, Object index, Object value) throws Throwable { // generally allow array access for now return super.onSetArray(invoker, receiver, index, value); } private static boolean hasProperty(Object object, String property) { if (InvokerHelper.getMetaClass(object).hasProperty(object, property) != null) return true; // The only way to be sure whether something is handled as a property in // Groovy is to actually get it and catch a MissingPropertyException. // But this actually accesses the property (-> side effects?)! // Here this is no problem, since we only disallow some write access... // The only allowed class with side effects should be InstanceAccessor, // which is in "allAllowedClasses" and thus shouldn't reach here try { InvokerHelper.getProperty(object, property); return true; } catch (MissingPropertyException e) { return false; } } private boolean isAllowedClass(Class<?> clazz) { // instanceAllowedClasses.add needs to be synchronized, as internal // state changes. // .contains does not need to be synchronized, worst case would be that // an element is added several times then, which doesn't matter. if (instanceAllowedClasses.contains(clazz)) return true; // allow accessing arrays in general // (calls like execute are disallowed by another mechanism) if (clazz.isArray()) return true; // allow nested classes of allowed classes Class<?> topLevelClass = clazz; while (topLevelClass.getEnclosingClass() != null) { topLevelClass = topLevelClass.getEnclosingClass(); } if (topLevelClass != clazz) { if (instanceAllowedClasses.contains(topLevelClass)) { // cache result for nested class synchronized (instanceAllowedClasses) { instanceAllowedClasses.add(clazz); } return true; } } // walk through prefixes for (AllowedPrefix allowedPackage : instanceAllowedPackages) { if (allowedPackage.checkAllowed(clazz)) { // cache result for class within a allowed package synchronized (instanceAllowedClasses) { instanceAllowedClasses.add(clazz); } return true; } } return false; } }