/* (c) 2015 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.config.util; import java.sql.Time; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.platform.GeoServerExtensions; import org.geotools.util.NumberRange; import org.geotools.util.SimpleInternationalString; import org.geotools.util.Version; import org.geotools.util.logging.Logging; import org.opengis.feature.type.Name; import org.opengis.filter.Filter; import org.opengis.referencing.crs.CoordinateReferenceSystem; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.ConverterLookup; import com.thoughtworks.xstream.converters.ConverterRegistry; import com.thoughtworks.xstream.converters.reflection.ReflectionProvider; import com.thoughtworks.xstream.core.ClassLoaderReference; import com.thoughtworks.xstream.io.HierarchicalStreamDriver; import com.thoughtworks.xstream.mapper.Mapper; import com.thoughtworks.xstream.mapper.MapperWrapper; import com.thoughtworks.xstream.security.ForbiddenClassException; import com.thoughtworks.xstream.security.NoTypePermission; import com.thoughtworks.xstream.security.PrimitiveTypePermission; /** * A XStream subclass allowing conversion of no class other than those explicitly registered using * the allowType* methods. To simplify the setup, it already allows the use of primitives, strings, * dates and collections * * @author Andrea Aime - GeoSolutions * */ public class SecureXStream extends XStream { private static final String WHITELIST_KEY = "GEOSERVER_XSTREAM_WHITELIST"; static final Logger LOGGER = Logging.getLogger(SecureXStream.class); public SecureXStream() { super(); init(); } public SecureXStream(HierarchicalStreamDriver hierarchicalStreamDriver) { super(hierarchicalStreamDriver); init(); } public SecureXStream(ReflectionProvider reflectionProvider, HierarchicalStreamDriver driver, ClassLoaderReference classLoaderReference, Mapper mapper, ConverterLookup converterLookup, ConverterRegistry converterRegistry) { super(reflectionProvider, driver, classLoaderReference, mapper, converterLookup, converterRegistry); init(); } public SecureXStream(ReflectionProvider reflectionProvider, HierarchicalStreamDriver driver, ClassLoaderReference classLoaderReference, Mapper mapper) { super(reflectionProvider, driver, classLoaderReference, mapper); init(); } public SecureXStream(ReflectionProvider reflectionProvider, HierarchicalStreamDriver driver, ClassLoaderReference classLoaderReference) { super(reflectionProvider, driver, classLoaderReference); init(); } public SecureXStream(ReflectionProvider reflectionProvider, HierarchicalStreamDriver hierarchicalStreamDriver) { super(reflectionProvider, hierarchicalStreamDriver); init(); } public SecureXStream(ReflectionProvider reflectionProvider) { super(reflectionProvider); init(); } private void init() { // by default, convert nothing addPermission(NoTypePermission.NONE); // the placeholder for null values allowTypes(new Class[] { Mapper.Null.class }); // allow primitives addPermission(new PrimitiveTypePermission()); // and common non primitives allowTypes(new Class[] { String.class, Date.class, java.sql.Date.class, Timestamp.class, Time.class }); // allow common GeoTools types too allowTypeHierarchy(Filter.class); allowTypeHierarchy(NumberRange.class); allowTypeHierarchy(CoordinateReferenceSystem.class); allowTypeHierarchy(Name.class); allowTypes(new Class[] { Version.class, SimpleInternationalString.class }); // common collection types allowTypes(new Class[] { TreeSet.class, SortedSet.class, Set.class, HashSet.class, LinkedHashSet.class, List.class, ArrayList.class, CopyOnWriteArrayList.class, Map.class, HashMap.class, TreeMap.class, ConcurrentHashMap.class, }); // Allow classes from user defined whitelist String whitelistProp = GeoServerExtensions.getProperty(WHITELIST_KEY); if (whitelistProp != null) { String[] wildcards = whitelistProp.split("\\s+|(\\s*;\\s*)"); this.allowTypesByWildcard(wildcards); } } @Override protected MapperWrapper wrapMapper(MapperWrapper next) { return new DetailedSecurityExceptionWrapper(next); } /** * A wrapper that adds instructions on what to do when a class was not part of the whitelist * * @author Andrea Aime - GeoSolutions */ static class DetailedSecurityExceptionWrapper extends MapperWrapper { public DetailedSecurityExceptionWrapper(Mapper wrapped) { super(wrapped); } @Override public Class realClass(String elementName) { try { return super.realClass(elementName); } catch (ForbiddenClassException e) { StringBuilder sb = new StringBuilder(); sb.append("Class {0} is not whitelisted for XML parsing. \n"); sb.append( "This is done to prevent Remote Code Execution attacks, but it might be \n") .append("you need this class to be authorized for GeoServer to actually work\n"); sb.append("If you are a user, you can set a variable named ").append(WHITELIST_KEY) .append("\n") .append(" with a semicolon separated list of fully qualified names, or patterns\n") .append(" to match several classes.The variable can be set as a system variable,\n") .append(" an environment variable, or a servlet context variable, just like\n") .append(" GEOSERVER_DATA_DIR.\n") .append(" For example, in order to authorize the org.geoserver.Foo class,\n") .append(" plus any class in the org.geoserver.custom package, one could set\n") .append(" a system variable: \n").append(" -D").append(WHITELIST_KEY) .append("=org.geoserver.Foo;org.geoserver.custom.**\n"); sb.append( "If instead you are a developer, you can call allowTypes/allowTypeHierarchy against\n") .append(" the XStream used for serialization by rolling a custom\n") .append(" XStreamPersisterInitializer or customizing your XStreamServiceLoader."); LOGGER.log(Level.SEVERE, sb.toString(), e.getMessage()); throw new ForbiddenClassExceptionEx( "Unauthorized class found, see logs for more details on how to handle it: " + e.getMessage(), e); } } } /** * Just to have a recognizable class for tests * * @author Andrea Aime - GeoSolutions * */ static class ForbiddenClassExceptionEx extends RuntimeException { public ForbiddenClassExceptionEx(String message, Throwable cause) { super(message, cause); } } }