Blob Blame History Raw
From b235f65141d07e5dc44b5965657bbe1e539cc966 Mon Sep 17 00:00:00 2001
From: Mikolaj Izdebski <mizdebsk@redhat.com>
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 @@
     </target>
 
     <target name="compile:java3" depends="compile:init" description="Compile all minimum Java 1.3 specific">
-        <javac srcdir="${java.src.dir}" destdir="${java.build.dir}" debug="${java.compile.debug}" source="1.3" target="1.3">
+        <javac srcdir="${java.src.dir}" destdir="${java.build.dir}" debug="${java.compile.debug}" source="1.5" target="1.5">
             <exclude name="com/thoughtworks/xstream/converters/reflection/HarmonyReflectionProvider.java"/>
             <excludesfile name="jdk-1.5-minimum.txt" />
             <excludesfile name="jdk-1.5-specific.txt" unless="jdk1.5.available" />
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. <em>Note, that this only applies if annotations are not 
- * auto-detected on -the-fly.</em>
+ * auto-detected on-the-fly.</em>
  * </p>
  * <h3>Implicit collections</h3>
  * <p/>
@@ -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.
+     * 
+     * <p>
+     * Permissions are evaluated in the added sequence. An instance of {@link NoTypePermission} or
+     * {@link AnyTypePermission} will implicitly wipe any existing permission.
+     * </p>
+     * 
+     * @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.
+     * <p>
+     * Supported are patterns with path expressions using dot as separator:
+     * </p>
+     * <ul>
+     * <li>?: one non-control character except separator, e.g. for 'java.net.Inet?Address'</li>
+     * <li>*: arbitrary number of non-control characters except separator, e.g. for types in a package like 'java.lang.*'</li>
+     * <li>**: arbitrary number of non-control characters including separator, e.g. for types in a package and subpackages like 'java.lang.**'</li>
+     * </ul>
+     * 
+     * @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.
+     * <p>
+     * Supported are patterns with path expressions using dot as separator:
+     * </p>
+     * <ul>
+     * <li>?: one non-control character except separator, e.g. for 'java.net.Inet?Address'</li>
+     * <li>*: arbitrary number of non-control characters except separator, e.g. for types in a package like 'java.lang.*'</li>
+     * <li>**: arbitrary number of non-control characters including separator, e.g. for types in a package and subpackages like 'java.lang.**'</li>
+     * </ul>
+     * 
+     * @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&ouml;rg Schaible
+ * @since upcoming
+ */
+public class SecurityMapper extends MapperWrapper {
+
+    private final List<TypePermission> 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<TypePermission>()
+            : new ArrayList<TypePermission>(Arrays.asList(permissions));
+    }
+
+    /**
+     * Add a new permission.
+     * <p>
+     * Permissions are evaluated in the added sequence. An instance of {@link NoTypePermission} or
+     * {@link AnyTypePermission} will implicitly wipe any existing permission.
+     * </p>
+     * 
+     * @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 <code>null</code>.
+ * 
+ * @author J&ouml;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&ouml;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&ouml;rg Schaible
+ * @since upcoming
+ */
+public class ExplicitTypePermission implements TypePermission {
+
+    final Set<String> names;
+    
+    /**
+     * @since upcoming
+     */
+    public ExplicitTypePermission(String...names) {
+        this.names = names == null ? Collections.<String>emptySet() : new HashSet<String>(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&ouml;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.
+ * <p>
+ * 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.
+ * </p>
+ * 
+ * @author J&ouml;rg Schaible
+ * @since upcoming
+ */
+public class NoPermission implements TypePermission {
+
+    private final TypePermission permission;
+
+    /**
+     * Construct a NoPermission.
+     * 
+     * @param permission the permission to negate or <code>null</code> 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.
+ * <p>
+ * Can be used to skip any existing default permission.
+ * </p>
+ * 
+ * @author J&ouml;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 <code>null</code> or XStream's null replacement type.
+ * 
+ * @author J&ouml;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&ouml;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&ouml;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&ouml;rg Schaible
+ * @since upcoming
+ */
+public interface TypePermission {
+    /**
+     * Check permission for a provided type.
+     * 
+     * @param type the type to check
+     * @return <code>true</code> if provided type is allowed, <code>false</code> 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.
+ * 
+ * <p>
+ * Supported are patterns with path expressions using dot as separator:
+ * </p>
+ * <ul>
+ * <li>?: one non-control character except separator, e.g. for 'java.net.Inet?Address'</li>
+ * <li>*: arbitrary number of non-control characters except separator, e.g. for types in a package like 'java.lang.*'</li>
+ * <li>**: arbitrary number of non-control characters including separator, e.g. for types in a package and subpackages like 'java.lang.**'</li>
+ * </ul>
+ * <p>
+ * The complete range of UTF-8 characters is supported except control characters.
+ * </p>
+ * 
+ * @author J&ouml;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&ouml;rg Schaible
+ */
+public class SecurityMapperTest extends TestCase {
+
+    private SecurityMapper mapper;
+    private Map<String, Class<?>> classMap;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        classMap = new HashMap<String, Class<?>>();
+        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