diff --git a/xstream-distribution/src/content/changes.html b/xstream-distribution/src/content/changes.html index 4147f92f5..bb64a293b 100644 --- a/xstream-distribution/src/content/changes.html +++ b/xstream-distribution/src/content/changes.html @@ -35,6 +35,7 @@
XStream is designed as a library, that is easy to use. It takes its main task very serious to convert from Java + objects to XML and back. As result it is possible that you create an instance of XStream with the default + constructor, call a method to turn an object into XML and call another one to turn the XML back into an equal Java + object. There are not a lot limits for those objects, XStream can handle nearly all.
+ +This flexibility comes at a price. XStream is using aggressive code internally like undocumented Java + features and reflection to be able to handle all kind of unknown types. The XML output contains by default any + information required to rebuild all these types. Regarding security we have now two different aspects:
+ +Always remember that manipulation of input data might happen on different levels, e.g. manipulation the value + of objects (e.g. exchanging a price value) or breaking the format causing the XML parser to fail. The latter + raises at least an error condition while the former must be catched with validity checks in case of sensitive + data. Even worse is an unrecognized injection resulting in a modified application execution with the worst case + of arbitrary command execution.
+ +An active SecurityManager can prevent actions required by XStream components or converters. Same applies for + an environment like Google Application Engine. XStream tries to some extend to check the functionality of a + converter before it claims to handle a type.
+ +Therefore it is possible that XStream behaves different in such an environment, because a converter suddenly no + longer handles a special type or any type at all. It is essential that an application that will have to run in such an environment is + tested at an early stage to prevent nasty surprises.
+ +As already explained it is possible to inject other object instances if someone has the possibility to + manipulate the data stream used to deserialize the Java objects (typically XML, but XStream supports other formats + like JSON). A known vulnerability can be created with the help of the Java runtime library using the Java Bean + EventHandler. As an instance + for the InvocationHandler + of a dynamic proxy it can be used to install a redirect for an arbitrary call to the original object to the method + of a completely different instance of an embedded object of the EventHandler itself.
+ +This scenario can be used perfectly to replace/inject a dynamic proxy with such an EventHandler at any location + in the XML where its parent expects an object of such an interface's type or a simple object instance (any list + element will suffice). The usage of a ProcessBuilder as embedded element and the redirection of any call to the + ProcessBuilder's start() + method allows even the call of shell commands. All you have to know is the XML representation of such a + combination.
+ +Starting with XStream 1.4.7 an instance of the EventHandler is no longer handled by default. You have to + register explicitly a ReflectionConverter for the EventHandler type, if your application has the requirement to + persist such an object. However, you have to take special care about the location of the persisted data and how + you can ensure its integrity.
+ +Note, that this vulnerability is not even a special problem of XStream. The XML acts here like + a script and the scenario above can be created with any script that is executed within a Java runtime (e.g. using + its JavaScript interpreter) if someone is able to manipulate it externally.
+ +While XStream implicitly avoids the vulnerability scenario with the EventHandler, there might be other + combinations with types from well-known and often used Java libraries like ASM, CGLIB, Groovy, or even in the Java + runtime that are currently simply unknown.
+ +Starting with XStream 1.4.7 it is possible to define permissions for types to check the + type of an object that should be unmarshalled. Those permissions can be used to allow or deny types explicitly. + With these permissions it is at least possible to inject types into an object graph that do not belong anywhere + into it. Any application that deserializes data from an external source should at least use this possibility to + limit the danger of arbitrary command execution.
+ +Apart from value manipulations, this implementation still allows the injection of allowed + objects at wrong locations. e.g. inserting an integer into a list of strings.
+ +Apart from the XStream security framework, it has always been possible to overwrite the setupConverter method of + XStream to register only the required converters.
+ +XML itself supports input validation using a schema and a validating parser. With XStream you can use e.g. a + StAX parser for validation, but it will take some effort to ensure that the XML read and written by XStream matches + the schema in first place. Typically you will have to write some custom converters, but it can be worth the effort + depending on the use case.
+ +As explained, it might be possible, that other combinations are found with the Java runtime itself or other + often used Java libraries that allow a similar vulnerability like the known case using the Java Beans EventHandler. + To prevent such a possibility at all, XStream contains since version 1.4.7 a security framework, where you can + define, which types are allowed to be unmarshalled with XStream.
+ +Core interface is TypePermission. + The SecurityMapper will evaluate a list + of registered instances for every type that will be required while unmarshalling input data. The interface has one + simple method:
boolean allow(Class<?>);
The XStream facade provides following methods to + register such type permissions within the SecurityMapper:
XStream.addPermission(TypePermission); +XStream.allowTypes(String...); +XStream.allowTypesByRegExp(String...); +XStream.allowTypesByRegExp(Pattern...); +XStream.allowTypesByWildcard(String...); +XStream.denyPermission(TypePermission); +XStream.denyTypes(String...); +XStream.denyTypesByRegExp(String...); +XStream.denyTypesByRegExp(Pattern...); +XStream.denyTypesByWildcard(String...);
The sequence of registration is essential. The latest registered permission will be evaluated first.
+ +Every TypePermission has three options to implement the allow method and make decisions on the provided type:
+
XStream provides some TypePermission implementations to allow any or no type at all, to allow primitive types + and their counterpart, null, array types, implementations match the name of the type by regular or wildcard + expression and one to invert a permission.
+ +Permission | +Description | +Example | +
---|---|---|
AnyTypePermission | +Allow any type. You may use the ANY instance directly. A registration of this permission will wipe any + prior one. | ++ |
ArrayTypePermission | +Allow any array type. | ++ |
CGLIBProxyTypePermission | +Allow any CGLIB proxy type. | ++ |
ExplicitTypePermission | +Allow types explicitly by name. | ++ |
HibernateProxyTypePermission | +Allow any Hibernate proxy type. Implementation is located in XStream's Hibernate extension. | ++ |
NoPermission | +Invert any other permission. Instances of this type are used by XStream in the deny methods. | ++ |
NullPermission | +Allow null as type. | ++ |
PrimitiveTypePermission | +Allow any primitive type and its boxed counterpart. | ++ |
ProxyTypePermission | +Allow any Java proxy type. | ++ |
RegExpTypePermission | +Allow any type that matches with its name a regular expression. | +.*\\.core\\..* [^$]+ |
+
WildcardTypePermission | +Allow any type that matches with its name a wildcard expression. | +java.lang.* java.util.** |
+
+ * 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: + *
+ *+ * Supported are patterns with path expressions using dot as separator: + *
+ *+ * 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); + } + + 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 000000000..21b36409b --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/security/AnyTypePermission.java @@ -0,0 +1,32 @@ +/* + * 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 andnull
.
+ *
+ * @author Jörg Schaible
+ * @since upcoming
+ */
+public class AnyTypePermission implements TypePermission {
+ /**
+ * @since upcoming
+ */
+ public static final TypePermission ANY = new AnyTypePermission();
+
+ public boolean allows(Class type) {
+ return true;
+ }
+
+ public int hashCode() {
+ return 3;
+ }
+
+ 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 000000000..34cc6288a
--- /dev/null
+++ b/xstream/src/java/com/thoughtworks/xstream/security/ArrayTypePermission.java
@@ -0,0 +1,32 @@
+/*
+ * 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();
+
+ public boolean allows(Class type) {
+ return type != null && type.isArray();
+ }
+
+ public int hashCode() {
+ return 13;
+ }
+
+ public boolean equals(Object obj) {
+ return obj != null && obj.getClass() == ArrayTypePermission.class;
+ }
+}
diff --git a/xstream/src/java/com/thoughtworks/xstream/security/CGLIBProxyTypePermission.java b/xstream/src/java/com/thoughtworks/xstream/security/CGLIBProxyTypePermission.java
new file mode 100644
index 000000000..3e521b972
--- /dev/null
+++ b/xstream/src/java/com/thoughtworks/xstream/security/CGLIBProxyTypePermission.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014 XStream Committers.
+ * All rights reserved.
+ *
+ * Created on 19. January 2014 by Joerg Schaible
+ */
+package com.thoughtworks.xstream.security;
+
+import net.sf.cglib.proxy.Proxy;
+
+
+/**
+ * Permission for any array type.
+ *
+ * @author Jörg Schaible
+ * @since upcoming
+ */
+public class CGLIBProxyTypePermission implements TypePermission {
+ /**
+ * @since upcoming
+ */
+ public static final TypePermission PROXIES = new CGLIBProxyTypePermission();
+
+ public boolean allows(final Class type) {
+ return type != null && Proxy.isProxyClass(type);
+ }
+
+ public int hashCode() {
+ return 19;
+ }
+
+ public boolean equals(final Object obj) {
+ return obj != null && obj.getClass() == CGLIBProxyTypePermission.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 000000000..1d8e4473c
--- /dev/null
+++ b/xstream/src/java/com/thoughtworks/xstream/security/ExplicitTypePermission.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 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+ * 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 ornull
to forbid any type
+ * @since upcoming
+ */
+ public NoPermission(final TypePermission permission) {
+ this.permission = permission;
+ }
+
+ 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 000000000..89e08c28b
--- /dev/null
+++ b/xstream/src/java/com/thoughtworks/xstream/security/NoTypePermission.java
@@ -0,0 +1,36 @@
+/*
+ * 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(); + + public boolean allows(Class type) { + throw new ForbiddenClassException(type); + } + + public int hashCode() { + return 1; + } + + 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 000000000..c9dc86dec --- /dev/null +++ b/xstream/src/java/com/thoughtworks/xstream/security/NullPermission.java @@ -0,0 +1,26 @@ +/* + * 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 fornull
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();
+
+ 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 000000000..4bdac2605
--- /dev/null
+++ b/xstream/src/java/com/thoughtworks/xstream/security/PrimitiveTypePermission.java
@@ -0,0 +1,34 @@
+/*
+ * 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();
+
+ public boolean allows(Class type) {
+ return type != null && type.isPrimitive() || Primitives.isBoxed(type);
+ }
+
+ public int hashCode() {
+ return 7;
+ }
+
+ public boolean equals(Object obj) {
+ return obj != null && obj.getClass() == PrimitiveTypePermission.class;
+ }
+}
diff --git a/xstream/src/java/com/thoughtworks/xstream/security/ProxyTypePermission.java b/xstream/src/java/com/thoughtworks/xstream/security/ProxyTypePermission.java
new file mode 100644
index 000000000..82c6501ff
--- /dev/null
+++ b/xstream/src/java/com/thoughtworks/xstream/security/ProxyTypePermission.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014 XStream Committers.
+ * All rights reserved.
+ *
+ * Created on 19. January 2014 by Joerg Schaible
+ */
+package com.thoughtworks.xstream.security;
+
+import java.lang.reflect.Proxy;
+
+
+/**
+ * Permission for any array type.
+ *
+ * @author Jörg Schaible
+ * @since upcoming
+ */
+public class ProxyTypePermission implements TypePermission {
+ /**
+ * @since upcoming
+ */
+ public static final TypePermission PROXIES = new ProxyTypePermission();
+
+ public boolean allows(final Class type) {
+ return type != null && Proxy.isProxyClass(type);
+ }
+
+ public int hashCode() {
+ return 17;
+ }
+
+ public boolean equals(final Object obj) {
+ return obj != null && obj.getClass() == ProxyTypePermission.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 000000000..151ccdb21
--- /dev/null
+++ b/xstream/src/java/com/thoughtworks/xstream/security/RegExpTypePermission.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+ }
+
+ 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 000000000..c38c8856b
--- /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 000000000..ffa93de3a
--- /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: + *
+ *+ * 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 000000000..ebde756ed --- /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