From b235f65141d07e5dc44b5965657bbe1e539cc966 Mon Sep 17 00:00:00 2001 From: Mikolaj Izdebski Date: Tue, 11 Feb 2014 10:44:19 +0100 Subject: [PATCH] Fix for CVE-2013-7285 Backported from upstream revision 2210 --- xstream/build.xml | 2 +- .../src/java/com/thoughtworks/xstream/XStream.java | 148 ++++++++++++++- .../thoughtworks/xstream/core/util/Primitives.java | 4 + .../thoughtworks/xstream/mapper/CachingMapper.java | 28 ++- .../xstream/mapper/SecurityMapper.java | 75 ++++++++ .../xstream/security/AnyTypePermission.java | 35 ++++ .../xstream/security/ArrayTypePermission.java | 35 ++++ .../xstream/security/ExplicitTypePermission.java | 38 ++++ .../xstream/security/ForbiddenClassException.java | 27 +++ .../xstream/security/NoPermission.java | 40 ++++ .../xstream/security/NoTypePermission.java | 39 ++++ .../xstream/security/NullPermission.java | 27 +++ .../xstream/security/PrimitiveTypePermission.java | 37 ++++ .../xstream/security/RegExpTypePermission.java | 49 +++++ .../xstream/security/TypePermission.java | 25 +++ .../xstream/security/WildcardTypePermission.java | 84 +++++++++ .../xstream/mapper/SecurityMapperTest.java | 207 +++++++++++++++++++++ 17 files changed, 890 insertions(+), 10 deletions(-) create mode 100644 xstream/src/java/com/thoughtworks/xstream/mapper/SecurityMapper.java create mode 100644 xstream/src/java/com/thoughtworks/xstream/security/AnyTypePermission.java create mode 100644 xstream/src/java/com/thoughtworks/xstream/security/ArrayTypePermission.java create mode 100644 xstream/src/java/com/thoughtworks/xstream/security/ExplicitTypePermission.java create mode 100644 xstream/src/java/com/thoughtworks/xstream/security/ForbiddenClassException.java create mode 100644 xstream/src/java/com/thoughtworks/xstream/security/NoPermission.java create mode 100644 xstream/src/java/com/thoughtworks/xstream/security/NoTypePermission.java create mode 100644 xstream/src/java/com/thoughtworks/xstream/security/NullPermission.java create mode 100644 xstream/src/java/com/thoughtworks/xstream/security/PrimitiveTypePermission.java create mode 100644 xstream/src/java/com/thoughtworks/xstream/security/RegExpTypePermission.java create mode 100644 xstream/src/java/com/thoughtworks/xstream/security/TypePermission.java create mode 100644 xstream/src/java/com/thoughtworks/xstream/security/WildcardTypePermission.java create mode 100644 xstream/src/test/com/thoughtworks/xstream/mapper/SecurityMapperTest.java diff --git a/xstream/build.xml b/xstream/build.xml index 18ef35a..eaa187e 100644 --- a/xstream/build.xml +++ b/xstream/build.xml @@ -62,7 +62,7 @@ - + diff --git a/xstream/src/java/com/thoughtworks/xstream/XStream.java b/xstream/src/java/com/thoughtworks/xstream/XStream.java index 506e612..8af0f6a 100644 --- a/xstream/src/java/com/thoughtworks/xstream/XStream.java +++ b/xstream/src/java/com/thoughtworks/xstream/XStream.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2003, 2004, 2005, 2006 Joe Walnes. - * Copyright (C) 2006, 2007, 2008 XStream Committers. + * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 XStream Committers. * All rights reserved. * * The software in this package is published under the terms of the BSD @@ -92,8 +92,16 @@ import com.thoughtworks.xstream.mapper.Mapper; import com.thoughtworks.xstream.mapper.MapperWrapper; import com.thoughtworks.xstream.mapper.OuterClassMapper; import com.thoughtworks.xstream.mapper.PackageAliasingMapper; +import com.thoughtworks.xstream.mapper.SecurityMapper; import com.thoughtworks.xstream.mapper.SystemAttributeAliasingMapper; import com.thoughtworks.xstream.mapper.XStream11XmlFriendlyMapper; +import com.thoughtworks.xstream.security.AnyTypePermission; +import com.thoughtworks.xstream.security.ExplicitTypePermission; +import com.thoughtworks.xstream.security.NoPermission; +import com.thoughtworks.xstream.security.NoTypePermission; +import com.thoughtworks.xstream.security.RegExpTypePermission; +import com.thoughtworks.xstream.security.TypePermission; +import com.thoughtworks.xstream.security.WildcardTypePermission; import java.io.EOFException; import java.io.File; @@ -132,6 +140,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.Vector; +import java.util.regex.Pattern; /** @@ -256,7 +265,7 @@ import java.util.Vector; * The XStream instance is thread-safe. That is, once the XStream instance has been created and * configured, it may be shared across multiple threads allowing objects to be * serialized/deserialized concurrently. Note, that this only applies if annotations are not - * auto-detected on -the-fly. + * auto-detected on-the-fly. *

*

Implicit collections

*

@@ -292,6 +301,7 @@ public class XStream { private ImmutableTypesMapper immutableTypesMapper; private ImplicitCollectionMapper implicitCollectionMapper; private LocalConversionMapper localConversionMapper; + private SecurityMapper securityMapper; private AnnotationConfiguration annotationConfiguration; private transient JVM jvm = new JVM(); @@ -440,6 +450,7 @@ public class XStream { this.mapper = mapper == null ? buildMapper() : mapper; setupMappers(); + setupSecurity(); setupAliases(); setupDefaultImplementations(); setupConverters(); @@ -470,6 +481,7 @@ public class XStream { } mapper = new LocalConversionMapper(mapper); mapper = new ImmutableTypesMapper(mapper); + mapper = new SecurityMapper(mapper); if (JVM.is15()) { mapper = buildMapperDynamically( ANNOTATION_MAPPER_TYPE, @@ -521,9 +533,19 @@ public class XStream { .lookupMapperOfType(ImmutableTypesMapper.class); localConversionMapper = (LocalConversionMapper)this.mapper .lookupMapperOfType(LocalConversionMapper.class); + securityMapper = (SecurityMapper)this.mapper + .lookupMapperOfType(SecurityMapper.class); annotationConfiguration = (AnnotationConfiguration)this.mapper .lookupMapperOfType(AnnotationConfiguration.class); } + + protected void setupSecurity() { + if (securityMapper == null) { + return; + } + + addPermission(AnyTypePermission.ANY); + } protected void setupAliases() { if (classAliasingMapper == null) { @@ -1620,4 +1642,126 @@ public class XStream { return this; } + /** + * Add a new security permission. + * + *

+ * Permissions are evaluated in the added sequence. An instance of {@link NoTypePermission} or + * {@link AnyTypePermission} will implicitly wipe any existing permission. + *

+ * + * @param permission the permission to add + * @since upcoming + */ + public void addPermission(TypePermission permission) { + if (securityMapper != null) { + securityMapper.addPermission(permission); + } + } + + /** + * Add security permission for explicit types by name. + * + * @param names the type names to allow + * @since upcoming + */ + public void allowTypes(String... names) { + addPermission(new ExplicitTypePermission(names)); + } + + /** + * Add security permission for types matching one of the specified regular expressions. + * + * @param regexps the regular expressions to allow type names + * @since upcoming + */ + public void allowTypesByRegExp(String... regexps) { + addPermission(new RegExpTypePermission(regexps)); + } + + /** + * Add security permission for types matching one of the specified regular expressions. + * + * @param regexps the regular expressions to allow type names + * @since upcoming + */ + public void allowTypesByRegExp(Pattern... regexps) { + addPermission(new RegExpTypePermission(regexps)); + } + + /** + * Add security permission for types matching one of the specified wildcard patterns. + *

+ * Supported are patterns with path expressions using dot as separator: + *

+ *
    + *
  • ?: one non-control character except separator, e.g. for 'java.net.Inet?Address'
  • + *
  • *: arbitrary number of non-control characters except separator, e.g. for types in a package like 'java.lang.*'
  • + *
  • **: arbitrary number of non-control characters including separator, e.g. for types in a package and subpackages like 'java.lang.**'
  • + *
+ * + * @param patterns the patterns to allow type names + * @since upcoming + */ + public void allowTypesByWildcard(String... patterns) { + addPermission(new WildcardTypePermission(patterns)); + } + + /** + * Add security permission denying another one. + * + * @param permission the permission to deny + * @since upcoming + */ + public void denyPermission(TypePermission permission) { + addPermission(new NoPermission(permission)); + } + + /** + * Add security permission forbidding explicit types by name. + * + * @param names the type names to forbid + * @since upcoming + */ + public void denyTypes(String... names) { + denyPermission(new ExplicitTypePermission(names)); + } + + /** + * Add security permission forbidding types matching one of the specified regular expressions. + * + * @param regexps the regular expressions to forbid type names + * @since upcoming + */ + public void denyTypesByRegExp(String... regexps) { + denyPermission(new RegExpTypePermission(regexps)); + } + + /** + * Add security permission forbidding types matching one of the specified regular expressions. + * + * @param regexps the regular expressions to forbid type names + * @since upcoming + */ + public void denyTypesByRegExp(Pattern... regexps) { + denyPermission(new RegExpTypePermission(regexps)); + } + + /** + * Add security permission forbidding types matching one of the specified wildcard patterns. + *

+ * Supported are patterns with path expressions using dot as separator: + *

+ *
    + *
  • ?: one non-control character except separator, e.g. for 'java.net.Inet?Address'
  • + *
  • *: arbitrary number of non-control characters except separator, e.g. for types in a package like 'java.lang.*'
  • + *
  • **: arbitrary number of non-control characters including separator, e.g. for types in a package and subpackages like 'java.lang.**'
  • + *
+ * + * @param patterns the patterns to forbid names + * @since upcoming + */ + public void denyTypesByWildcard(String... patterns) { + denyPermission(new WildcardTypePermission(patterns)); + } } diff --git a/xstream/src/java/com/thoughtworks/xstream/core/util/Primitives.java b/xstream/src/java/com/thoughtworks/xstream/core/util/Primitives.java index f6e988e..e0f5fbf 100644 --- a/xstream/src/java/com/thoughtworks/xstream/core/util/Primitives.java +++ b/xstream/src/java/com/thoughtworks/xstream/core/util/Primitives.java @@ -48,4 +48,8 @@ public final class Primitives { static public Class unbox(final Class type) { return (Class)UNBOX.get(type); } + + static public boolean isBoxed(final Class type) { + return UNBOX.keySet().contains(type); + } } diff --git a/xstream/src/java/com/thoughtworks/xstream/mapper/CachingMapper.java b/xstream/src/java/com/thoughtworks/xstream/mapper/CachingMapper.java index 7e865c7..a90c194 100644 --- a/xstream/src/java/com/thoughtworks/xstream/mapper/CachingMapper.java +++ b/xstream/src/java/com/thoughtworks/xstream/mapper/CachingMapper.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2005 Joe Walnes. - * Copyright (C) 2006, 2007, 2008 XStream Committers. + * Copyright (C) 2006, 2007, 2008, 2009, 2011, 2013, 2014 XStream Committers. * All rights reserved. * * The software in this package is published under the terms of the BSD @@ -18,6 +18,9 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import com.thoughtworks.xstream.XStreamException; +import com.thoughtworks.xstream.security.ForbiddenClassException; + /** * Mapper that caches which names map to which classes. Prevents repetitive searching and class loading. * @@ -43,15 +46,26 @@ public class CachingMapper extends MapperWrapper { public Class realClass(String elementName) { WeakReference reference = (WeakReference) realClassCache.get(elementName); if (reference != null) { - Class cached = (Class) reference.get(); + Object cached = reference.get(); if (cached != null) { - return cached; + if (cached instanceof Class) { + return (Class)cached; + } + throw (XStreamException)cached; } } - - Class result = super.realClass(elementName); - realClassCache.put(elementName, new WeakReference(result)); - return result; + + try { + Class result = super.realClass(elementName); + realClassCache.put(elementName, result); + return result; + } catch (ForbiddenClassException e) { + realClassCache.put(elementName, e); + throw e; + } catch (CannotResolveClassException e) { + realClassCache.put(elementName, e); + throw e; + } } private Object readResolve() { diff --git a/xstream/src/java/com/thoughtworks/xstream/mapper/SecurityMapper.java b/xstream/src/java/com/thoughtworks/xstream/mapper/SecurityMapper.java new file mode 100644 index 0000000..151bb91 --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/mapper/SecurityMapper.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2014 XStream Committers. + * All rights reserved. + * + * Created on 08. January 2014 by Joerg Schaible + */ +package com.thoughtworks.xstream.mapper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.thoughtworks.xstream.security.AnyTypePermission; +import com.thoughtworks.xstream.security.ForbiddenClassException; +import com.thoughtworks.xstream.security.NoTypePermission; +import com.thoughtworks.xstream.security.TypePermission; + + +/** + * @author Jörg Schaible + * @since upcoming + */ +public class SecurityMapper extends MapperWrapper { + + private final List permissions; + + /** + * Construct a SecurityMapper. + * + * @param wrapped the mapper chain + * @since upcoming + */ + public SecurityMapper(final Mapper wrapped) { + this(wrapped, (TypePermission[])null); + } + + /** + * Construct a SecurityMapper. + * + * @param wrapped the mapper chain + * @param permissions the predefined permissions + * @since upcoming + */ + public SecurityMapper(final Mapper wrapped, final TypePermission... permissions) { + super(wrapped); + this.permissions = permissions == null // + ? new ArrayList() + : new ArrayList(Arrays.asList(permissions)); + } + + /** + * Add a new permission. + *

+ * Permissions are evaluated in the added sequence. An instance of {@link NoTypePermission} or + * {@link AnyTypePermission} will implicitly wipe any existing permission. + *

+ * + * @param permission the permission to add. + * @since upcoming + */ + public void addPermission(final TypePermission permission) { + if (permission.equals(NoTypePermission.NONE) || permission.equals(AnyTypePermission.ANY)) + permissions.clear(); + permissions.add(permission); + } + + @Override + public Class realClass(final String elementName) { + final Class type = super.realClass(elementName); + for (final TypePermission permission : permissions) + if (permission.allows(type)) + return type; + throw new ForbiddenClassException(type); + } +} diff --git a/xstream/src/java/com/thoughtworks/xstream/security/AnyTypePermission.java b/xstream/src/java/com/thoughtworks/xstream/security/AnyTypePermission.java new file mode 100644 index 0000000..a5ac1b6 --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/security/AnyTypePermission.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 XStream Committers. + * All rights reserved. + * + * Created on 08. January 2014 by Joerg Schaible + */ +package com.thoughtworks.xstream.security; + +/** + * Permission for any type and null. + * + * @author Jörg Schaible + * @since upcoming + */ +public class AnyTypePermission implements TypePermission { + /** + * @since upcoming + */ + public static final TypePermission ANY = new AnyTypePermission(); + + @Override + public boolean allows(Class type) { + return true; + } + + @Override + public int hashCode() { + return 3; + } + + @Override + public boolean equals(Object obj) { + return obj != null && obj.getClass() == AnyTypePermission.class; + } +} diff --git a/xstream/src/java/com/thoughtworks/xstream/security/ArrayTypePermission.java b/xstream/src/java/com/thoughtworks/xstream/security/ArrayTypePermission.java new file mode 100644 index 0000000..0b425ef --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/security/ArrayTypePermission.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 XStream Committers. + * All rights reserved. + * + * Created on 09. January 2014 by Joerg Schaible + */ +package com.thoughtworks.xstream.security; + +/** + * Permission for any array type. + * + * @author Jörg Schaible + * @since upcoming + */ +public class ArrayTypePermission implements TypePermission { + /** + * @since upcoming + */ + public static final TypePermission ARRAYS = new ArrayTypePermission(); + + @Override + public boolean allows(Class type) { + return type != null && type.isArray(); + } + + @Override + public int hashCode() { + return 13; + } + + @Override + public boolean equals(Object obj) { + return obj != null && obj.getClass() == ArrayTypePermission.class; + } +} diff --git a/xstream/src/java/com/thoughtworks/xstream/security/ExplicitTypePermission.java b/xstream/src/java/com/thoughtworks/xstream/security/ExplicitTypePermission.java new file mode 100644 index 0000000..4294d72 --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/security/ExplicitTypePermission.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2014 XStream Committers. + * All rights reserved. + * + * Created on 09. January 2014 by Joerg Schaible + */ +package com.thoughtworks.xstream.security; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Explicit permission for a type with a name matching one in the provided list. + * + * @author Jörg Schaible + * @since upcoming + */ +public class ExplicitTypePermission implements TypePermission { + + final Set names; + + /** + * @since upcoming + */ + public ExplicitTypePermission(String...names) { + this.names = names == null ? Collections.emptySet() : new HashSet(Arrays.asList(names)); + } + + @Override + public boolean allows(Class type) { + if (type == null) + return false; + return names.contains(type.getName()); + } + +} diff --git a/xstream/src/java/com/thoughtworks/xstream/security/ForbiddenClassException.java b/xstream/src/java/com/thoughtworks/xstream/security/ForbiddenClassException.java new file mode 100644 index 0000000..041e47a --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/security/ForbiddenClassException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2014 XStream Committers. + * All rights reserved. + * + * Created on 08. January 2014 by Joerg Schaible + */ +package com.thoughtworks.xstream.security; + +import com.thoughtworks.xstream.XStreamException; + +/** + * Exception thrown for a forbidden class. + * + * @author Jörg Schaible + * @since upcoming + */ +public class ForbiddenClassException extends XStreamException { + + /** + * Construct a ForbiddenClassException. + * @param type the forbidden class + * @since upcoming + */ + public ForbiddenClassException(Class type) { + super(type == null ? "null" : type.getName()); + } +} diff --git a/xstream/src/java/com/thoughtworks/xstream/security/NoPermission.java b/xstream/src/java/com/thoughtworks/xstream/security/NoPermission.java new file mode 100644 index 0000000..4845112 --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/security/NoPermission.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014 XStream Committers. + * All rights reserved. + * + * Created on 09. January 2014 by Joerg Schaible + */ +package com.thoughtworks.xstream.security; + +/** + * Wrapper to negate another type permission. + *

+ * If the wrapped {@link TypePermission} allows the type, this instance will throw a {@link ForbiddenClassException} + * instead. An instance of this permission cannot be used to allow a type. + *

+ * + * @author Jörg Schaible + * @since upcoming + */ +public class NoPermission implements TypePermission { + + private final TypePermission permission; + + /** + * Construct a NoPermission. + * + * @param permission the permission to negate or null to forbid any type + * @since upcoming + */ + public NoPermission(final TypePermission permission) { + this.permission = permission; + } + + @Override + public boolean allows(final Class type) { + if (permission == null || permission.allows(type)) { + throw new ForbiddenClassException(type); + } + return false; + } +} diff --git a/xstream/src/java/com/thoughtworks/xstream/security/NoTypePermission.java b/xstream/src/java/com/thoughtworks/xstream/security/NoTypePermission.java new file mode 100644 index 0000000..bfaa7e7 --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/security/NoTypePermission.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 XStream Committers. + * All rights reserved. + * + * Created on 08. January 2014 by Joerg Schaible + */ +package com.thoughtworks.xstream.security; + +/** + * No permission for any type. + *

+ * Can be used to skip any existing default permission. + *

+ * + * @author Jörg Schaible + * @since upcoming + */ +public class NoTypePermission implements TypePermission { + + /** + * @since upcoming + */ + public static final TypePermission NONE = new NoTypePermission(); + + @Override + public boolean allows(Class type) { + throw new ForbiddenClassException(type); + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object obj) { + return obj != null && obj.getClass() == NoTypePermission.class; + } +} diff --git a/xstream/src/java/com/thoughtworks/xstream/security/NullPermission.java b/xstream/src/java/com/thoughtworks/xstream/security/NullPermission.java new file mode 100644 index 0000000..a1b0372 --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/security/NullPermission.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2014 XStream Committers. + * All rights reserved. + * + * Created on 09. January 2014 by Joerg Schaible + */ +package com.thoughtworks.xstream.security; + +import com.thoughtworks.xstream.mapper.Mapper; + +/** + * Permission for null or XStream's null replacement type. + * + * @author Jörg Schaible + * @since upcoming + */ +public class NullPermission implements TypePermission { + /** + * @since upcoming + */ + public static final TypePermission NULL = new NullPermission(); + + @Override + public boolean allows(Class type) { + return type == null || type == Mapper.Null.class; + } +} diff --git a/xstream/src/java/com/thoughtworks/xstream/security/PrimitiveTypePermission.java b/xstream/src/java/com/thoughtworks/xstream/security/PrimitiveTypePermission.java new file mode 100644 index 0000000..58257c2 --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/security/PrimitiveTypePermission.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 XStream Committers. + * All rights reserved. + * + * Created on 09. January 2014 by Joerg Schaible + */ +package com.thoughtworks.xstream.security; + +import com.thoughtworks.xstream.core.util.Primitives; + +/** + * Permission for any primitive type and its boxed counterpart. + * + * @author Jörg Schaible + * @since upcoming + */ +public class PrimitiveTypePermission implements TypePermission { + /** + * @since upcoming + */ + public static final TypePermission PRIMITIVES = new PrimitiveTypePermission(); + + @Override + public boolean allows(Class type) { + return type != null && type.isPrimitive() || Primitives.isBoxed(type); + } + + @Override + public int hashCode() { + return 7; + } + + @Override + public boolean equals(Object obj) { + return obj != null && obj.getClass() == PrimitiveTypePermission.class; + } +} diff --git a/xstream/src/java/com/thoughtworks/xstream/security/RegExpTypePermission.java b/xstream/src/java/com/thoughtworks/xstream/security/RegExpTypePermission.java new file mode 100644 index 0000000..d5c8b57 --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/security/RegExpTypePermission.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 XStream Committers. + * All rights reserved. + * + * Created on 09. January 2014 by Joerg Schaible + */ +package com.thoughtworks.xstream.security; + +import java.util.regex.Pattern; + + +/** + * Permission for any type with a name matching one of the provided regular expressions. + * + * @author Jörg Schaible + * @since upcoming + */ +public class RegExpTypePermission implements TypePermission { + + private final Pattern[] patterns; + + public RegExpTypePermission(final String... patterns) { + this(getPatterns(patterns)); + } + + public RegExpTypePermission(final Pattern... patterns) { + this.patterns = patterns == null ? new Pattern[0] : patterns; + } + + @Override + public boolean allows(final Class type) { + if (type != null) { + final String name = type.getName(); + for (final Pattern pattern : patterns) + if (pattern.matcher(name).matches()) + return true; + } + return false; + } + + private static Pattern[] getPatterns(final String... patterns) { + if (patterns == null) + return null; + final Pattern[] array = new Pattern[patterns.length]; + for (int i = 0; i < array.length; ++i) + array[i] = Pattern.compile(patterns[i]); + return array; + } +} diff --git a/xstream/src/java/com/thoughtworks/xstream/security/TypePermission.java b/xstream/src/java/com/thoughtworks/xstream/security/TypePermission.java new file mode 100644 index 0000000..7246c22 --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/security/TypePermission.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2014 XStream Committers. + * All rights reserved. + * + * Created on 08. January 2014 by Joerg Schaible + */ +package com.thoughtworks.xstream.security; + +/** + * Definition of a type permission. + * + * @author Jörg Schaible + * @since upcoming + */ +public interface TypePermission { + /** + * Check permission for a provided type. + * + * @param type the type to check + * @return true if provided type is allowed, false if permission does not handle the type + * @throws ForbiddenClassException if provided type is explicitly forbidden + * @since upcoming + */ + boolean allows(Class type); +} diff --git a/xstream/src/java/com/thoughtworks/xstream/security/WildcardTypePermission.java b/xstream/src/java/com/thoughtworks/xstream/security/WildcardTypePermission.java new file mode 100644 index 0000000..ffa93de --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/security/WildcardTypePermission.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2014 XStream Committers. + * All rights reserved. + * + * Created on 09. January 2014 by Joerg Schaible + */ +package com.thoughtworks.xstream.security; + +/** + * Permission for any type with a name matching one of the provided wildcard expressions. + * + *

+ * Supported are patterns with path expressions using dot as separator: + *

+ *
    + *
  • ?: one non-control character except separator, e.g. for 'java.net.Inet?Address'
  • + *
  • *: arbitrary number of non-control characters except separator, e.g. for types in a package like 'java.lang.*'
  • + *
  • **: arbitrary number of non-control characters including separator, e.g. for types in a package and subpackages like 'java.lang.**'
  • + *
+ *

+ * The complete range of UTF-8 characters is supported except control characters. + *

+ * + * @author Jörg Schaible + * @since upcoming + */ +public class WildcardTypePermission extends RegExpTypePermission { + + /** + * @since upcoming + */ + public WildcardTypePermission(final String... patterns) { + super(getRegExpPatterns(patterns)); + } + + private static String[] getRegExpPatterns(final String... wildcards) { + if (wildcards == null) + return null; + final String[] regexps = new String[wildcards.length]; + for (int i = 0; i < wildcards.length; ++i) { + final String wildcardExpression = wildcards[i]; + final StringBuilder result = new StringBuilder(wildcardExpression.length() * 2); + result.append("(?u)"); + final int length = wildcardExpression.length(); + for (int j = 0; j < length; j++) { + final char ch = wildcardExpression.charAt(j); + switch (ch) { + case '\\': + case '.': + case '+': + case '|': + case '[': + case ']': + case '(': + case ')': + case '^': + case '$': + result.append('\\').append(ch); + break; + + case '?': + result.append('.'); + break; + + case '*': + // see "General Category Property" in http://www.unicode.org/reports/tr18/ + if (j + 1 < length && wildcardExpression.charAt(j + 1) == '*') { + result.append("[\\P{C}]*"); + j++; + } else { + result.append("[\\P{C}&&[^").append('.').append("]]*"); + } + break; + + default: + result.append(ch); + break; + } + } + regexps[i] = result.toString(); + } + return regexps; + } +} diff --git a/xstream/src/test/com/thoughtworks/xstream/mapper/SecurityMapperTest.java b/xstream/src/test/com/thoughtworks/xstream/mapper/SecurityMapperTest.java new file mode 100644 index 0000000..ebde756 --- /dev/null +++ b/xstream/src/test/com/thoughtworks/xstream/mapper/SecurityMapperTest.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2014 XStream Committers. + * All rights reserved. + * + * Created on 09. January 2014 by Joerg Schaible + */ +package com.thoughtworks.xstream.mapper; + +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.thoughtworks.xstream.core.JVM; +import com.thoughtworks.xstream.core.util.QuickWriter; +import com.thoughtworks.xstream.security.AnyTypePermission; +import com.thoughtworks.xstream.security.ArrayTypePermission; +import com.thoughtworks.xstream.security.ExplicitTypePermission; +import com.thoughtworks.xstream.security.ForbiddenClassException; +import com.thoughtworks.xstream.security.NoTypePermission; +import com.thoughtworks.xstream.security.NullPermission; +import com.thoughtworks.xstream.security.PrimitiveTypePermission; +import com.thoughtworks.xstream.security.RegExpTypePermission; +import com.thoughtworks.xstream.security.TypePermission; +import com.thoughtworks.xstream.security.WildcardTypePermission; + +import junit.framework.TestCase; + + +/** + * Tests the {@link SecurityMapper} and the {@link TypePermission} implementations. + * + * @author Jörg Schaible + */ +public class SecurityMapperTest extends TestCase { + + private SecurityMapper mapper; + private Map> classMap; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + classMap = new HashMap>(); + mapper = new SecurityMapper(new MapperWrapper(null) { + @Override + public Class realClass(final String elementName) { + return classMap.get(elementName); + } + }); + } + + private void register(final Class... types) { + for (final Class type : types) { + classMap.put(type.getName(), type); + } + } + + public void testAnyType() { + register(String.class, URL.class, List.class); + mapper.addPermission(NoTypePermission.NONE); + mapper.addPermission(AnyTypePermission.ANY); + assertSame(String.class, mapper.realClass(String.class.getName())); + assertSame(List.class, mapper.realClass(List.class.getName())); + assertNull(mapper.realClass(null)); + } + + public void testNoType() { + register(String.class, URL.class, List.class); + mapper.addPermission(NoTypePermission.NONE); + try { + mapper.realClass(String.class.getName()); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals(String.class.getName(), e.getMessage()); + } + try { + mapper.realClass(null); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals("null", e.getMessage()); + } + } + + public void testNullType() { + register(String.class, Mapper.Null.class); + mapper.addPermission(NullPermission.NULL); + assertSame(Mapper.Null.class, mapper.realClass(Mapper.Null.class.getName())); + assertNull(mapper.realClass(null)); + try { + mapper.realClass(String.class.getName()); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals(String.class.getName(), e.getMessage()); + } + } + + public void testPrimitiveTypes() { + register(String.class, int.class, Integer.class, char[].class, Character[].class); + mapper.addPermission(PrimitiveTypePermission.PRIMITIVES); + assertSame(int.class, mapper.realClass(int.class.getName())); + assertSame(Integer.class, mapper.realClass(Integer.class.getName())); + try { + mapper.realClass(String.class.getName()); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals(String.class.getName(), e.getMessage()); + } + try { + mapper.realClass(null); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals("null", e.getMessage()); + } + try { + mapper.realClass(char[].class.getName()); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals(char[].class.getName(), e.getMessage()); + } + } + + public void testArrayTypes() { + register(String.class, int.class, Integer.class, char[].class, Character[].class); + mapper.addPermission(ArrayTypePermission.ARRAYS); + assertSame(char[].class, mapper.realClass(char[].class.getName())); + assertSame(Character[].class, mapper.realClass(Character[].class.getName())); + try { + mapper.realClass(String.class.getName()); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals(String.class.getName(), e.getMessage()); + } + try { + mapper.realClass(null); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals("null", e.getMessage()); + } + try { + mapper.realClass(int.class.getName()); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals(int.class.getName(), e.getMessage()); + } + } + + public void testExplicitTypes() { + register(String.class, List.class); + mapper.addPermission(new ExplicitTypePermission(String.class.getName(), List.class.getName())); + assertSame(String.class, mapper.realClass(String.class.getName())); + assertSame(List.class, mapper.realClass(List.class.getName())); + try { + mapper.realClass(null); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals("null", e.getMessage()); + } + } + + public void testNamesWithRegExps() { + class Foo$_0 {} + final Class anonymous = new Object() {}.getClass(); + register(String.class, JVM.class, QuickWriter.class, Foo$_0.class, anonymous, DefaultClassMapperTest.class); + mapper.addPermission(new RegExpTypePermission(".*Test", ".*\\.core\\..*", ".*SecurityMapperTest\\$.+")); + assertSame(DefaultClassMapperTest.class, mapper.realClass(DefaultClassMapperTest.class.getName())); + assertSame(JVM.class, mapper.realClass(JVM.class.getName())); + assertSame(QuickWriter.class, mapper.realClass(QuickWriter.class.getName())); + assertSame(Foo$_0.class, mapper.realClass(Foo$_0.class.getName())); + assertSame(anonymous, mapper.realClass(anonymous.getName())); + try { + mapper.realClass(String.class.getName()); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals(String.class.getName(), e.getMessage()); + } + } + + public void testNamesWithWildcardPatterns() { + class Foo$_0 {} + class Foo$_1 {} + final Class anonymous = new Object() {}.getClass(); + register(String.class, JVM.class, QuickWriter.class, Foo$_0.class, Foo$_1.class, anonymous); + mapper.addPermission(new WildcardTypePermission("**.*_0", "**.core.*", "**.SecurityMapperTest$?")); + assertSame(JVM.class, mapper.realClass(JVM.class.getName())); + assertSame(Foo$_0.class, mapper.realClass(Foo$_0.class.getName())); + assertSame(anonymous, mapper.realClass(anonymous.getName())); + try { + mapper.realClass(String.class.getName()); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals(String.class.getName(), e.getMessage()); + } + try { + mapper.realClass(QuickWriter.class.getName()); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals(QuickWriter.class.getName(), e.getMessage()); + } + try { + mapper.realClass(Foo$_1.class.getName()); + fail("Thrown " + ForbiddenClassException.class.getName() + " expected"); + } catch (final ForbiddenClassException e) { + assertEquals(Foo$_1.class.getName(), e.getMessage()); + } + } +} -- 1.8.4.2