Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2003, 2004, 2005 Joe Walnes.
* Copyright (C) 2006, 2007, 2010, 2011, 2013, 2018 XStream Committers.
* Copyright (C) 2006, 2007, 2010, 2011, 2013, 2018, 2021 XStream Committers.
* All rights reserved.
*
* The software in this package is published under the terms of the BSD
Expand All @@ -13,6 +13,7 @@

import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.core.SecurityUtils;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.mapper.Mapper;
Expand Down Expand Up @@ -96,7 +97,10 @@ protected void populateCollection(HierarchicalStreamReader reader, Unmarshalling
protected void addCurrentElementToCollection(HierarchicalStreamReader reader, UnmarshallingContext context,
Collection collection, Collection target) {
final Object item = readItem(reader, context, collection); // call readBareItem when deprecated method is removed

long now = System.currentTimeMillis();
target.add(item);
SecurityUtils.checkForCollectionDoSAttack(context, now);
}

protected Object createCollection(Class type) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2003, 2004, 2005 Joe Walnes.
* Copyright (C) 2006, 2007, 2008, 2010, 2011, 2012, 2013, 2018 XStream Committers.
* Copyright (C) 2006, 2007, 2008, 2010, 2011, 2012, 2013, 2018, 2021 XStream Committers.
* All rights reserved.
*
* The software in this package is published under the terms of the BSD
Expand All @@ -13,6 +13,7 @@

import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.core.SecurityUtils;
import com.thoughtworks.xstream.io.ExtendedHierarchicalStreamWriterHelper;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
Expand Down Expand Up @@ -104,7 +105,10 @@ protected void putCurrentEntryIntoMap(HierarchicalStreamReader reader, Unmarshal
Map map, Map target) {
final Object key = readCompleteItem(reader, context, map);
final Object value = readCompleteItem(reader, context, map);

long now = System.currentTimeMillis();
target.put(key, value);
SecurityUtils.checkForCollectionDoSAttack(context, now);
}

protected Object createCollection(Class type) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013, 2016, 2018 XStream Committers.
* Copyright (C) 2013, 2016, 2018, 2021 XStream Committers.
* All rights reserved.
*
* The software in this package is published under the terms of the BSD
Expand All @@ -21,6 +21,7 @@
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.collections.MapConverter;
import com.thoughtworks.xstream.core.JVM;
import com.thoughtworks.xstream.core.SecurityUtils;
import com.thoughtworks.xstream.core.util.HierarchicalStreams;
import com.thoughtworks.xstream.io.ExtendedHierarchicalStreamWriterHelper;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
Expand Down Expand Up @@ -339,7 +340,9 @@ protected void populateMap(HierarchicalStreamReader reader, UnmarshallingContext
value = valueConverter.fromString(reader.getValue());
}

long now = System.currentTimeMillis();
target.put(key, value);
SecurityUtils.checkForCollectionDoSAttack(context, now);

if (entryName != null) {
reader.moveUp();
Expand Down
56 changes: 56 additions & 0 deletions xstream/src/java/com/thoughtworks/xstream/core/SecurityUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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 21. September 2021 by Joerg Schaible
*/
package com.thoughtworks.xstream.core;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.security.InputManipulationException;


/**
* Utility functions for security issues.
*
* @author Jörg Schaible
* @since upcoming
*/
public class SecurityUtils {

/**
* Check the consumed time adding elements to collections or maps.
*
* Every custom converter should call this method after an unmarshalled element has been added to a collection or
* map. In case of an attack the operation will take too long, because the calculation of the hash code or the
* comparison of the elements in the collection operate on recursive structures.
*
* @param context the unmarshalling context
* @param start the timestamp just before the element was added to the collection or map
* @since upcoming
*/
public static void checkForCollectionDoSAttack(final UnmarshallingContext context, final long start) {
final int diff = (int)((System.currentTimeMillis() - start) / 1000);
if (diff > 0) {
final Integer secondsUsed = (Integer)context.get(XStream.COLLECTION_UPDATE_SECONDS);
if (secondsUsed != null) {
final Integer limit = (Integer)context.get(XStream.COLLECTION_UPDATE_LIMIT);
if (limit == null) {
throw new ConversionException("Missing limit for updating collections.");
}
final int seconds = secondsUsed.intValue() + diff;
if (seconds > limit.intValue()) {
throw new InputManipulationException(
"Denial of Service attack assumed. Adding elements to collections or maps exceeds " + limit.intValue() + " seconds.");
}
context.put(XStream.COLLECTION_UPDATE_SECONDS, new Integer(seconds));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.thoughtworks.xstream.core.util.PrioritizedList;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.mapper.Mapper;
import com.thoughtworks.xstream.security.AbstractSecurityException;


public class TreeUnmarshaller implements UnmarshallingContext {
Expand Down Expand Up @@ -74,6 +75,8 @@ protected Object convert(Object parent, Class type, Converter converter) {
} catch (final ConversionException conversionException) {
addInformationTo(conversionException, type, converter, parent);
throw conversionException;
} catch (AbstractSecurityException e) {
throw e;
} catch (RuntimeException e) {
ConversionException conversionException = new ConversionException(e);
addInformationTo(conversionException, type, converter, parent);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2021 XStream Committers.
* All rights reserved.
*
* Created on 21. September 2021 by Joerg Schaible
*/
package com.thoughtworks.xstream.security;

import com.thoughtworks.xstream.XStreamException;


/**
* General base class for a Security Exception in XStream.
*
* @author Jörg Schaible
* @since upcoming
*/
public abstract class AbstractSecurityException extends XStreamException {
private static final long serialVersionUID = 20210921L;

/**
* Constructs a SecurityException.
* @param message the exception message
* @since upcoming
*/
public AbstractSecurityException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
/*
* Copyright (C) 2014 XStream Committers.
* Copyright (C) 2014, 2021 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 1.4.7
*/
public class ForbiddenClassException extends XStreamException {
public class ForbiddenClassException extends AbstractSecurityException {

/**
* Construct a ForbiddenClassException.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2021 XStream Committers.
* All rights reserved.
*
* Created on 21. September 2021 by Joerg Schaible
*/
package com.thoughtworks.xstream.security;


/**
* Class for a Security Exception assuming input manipulation in XStream.
*
* @author Jörg Schaible
* @since upcoming
*/
public class InputManipulationException extends AbstractSecurityException {
private static final long serialVersionUID = 20210921L;

/**
* Constructs a SecurityException.
* @param message the exception message
* @since upcoming
*/
public InputManipulationException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,20 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import com.thoughtworks.xstream.XStreamException;
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.core.JVM;
import com.thoughtworks.xstream.security.AnyTypePermission;
import com.thoughtworks.xstream.security.ForbiddenClassException;
import com.thoughtworks.xstream.security.InputManipulationException;
import com.thoughtworks.xstream.security.ProxyTypePermission;


Expand Down Expand Up @@ -56,9 +63,9 @@ public void testCannotInjectEventHandler() {

try {
xstream.fromXML(xml);
fail("Thrown " + XStreamException.class.getName() + " expected");
} catch (final XStreamException e) {
assertTrue(e.getMessage().indexOf(EventHandler.class.getName()) > 0);
fail("Thrown " + ForbiddenClassException.class.getName() + " expected");
} catch (final ForbiddenClassException e) {
// OK
}
assertEquals(0, BUFFER.length());
}
Expand Down Expand Up @@ -126,7 +133,7 @@ public void exec() {
public void testInstanceOfVoid() {
try {
xstream.fromXML("<void/>");
fail("Thrown " + ConversionException.class.getName() + " expected");
fail("Thrown " + ForbiddenClassException.class.getName() + " expected");
} catch (final ForbiddenClassException e) {
// OK
}
Expand Down Expand Up @@ -163,7 +170,7 @@ public void testCannotUseJaxwsInputStreamToDeleteFile() {
xstream.aliasType("is", InputStream.class);
try {
xstream.fromXML(xml);
fail("Thrown " + ConversionException.class.getName() + " expected");
fail("Thrown " + ForbiddenClassException.class.getName() + " expected");
} catch (final ForbiddenClassException e) {
// OK
}
Expand Down Expand Up @@ -261,4 +268,140 @@ public void testExplicitlyUnmarshalEndlessByteArryInputStream() {
assertEquals("ArrayIndexOutOfBoundsException expected reading invalid stream", 5, i);
}
}

public void testDoSAttackWithHashSet() {
final Set set = new HashSet();
Set s1 = set;
Set s2 = new HashSet();
for (int i = 0; i < 30; i++) {
final Set t1 = new HashSet();
final Set t2 = new HashSet();
t1.add("a");
t2.add("b");
s1.add(t1);
s1.add(t2);
s2.add(t2);
s2.add(t1);
s1 = t1;
s2 = t2;
}

xstream.setCollectionUpdateLimit(5);
final String xml = xstream.toXML(set);
try {

xstream.fromXML(xml);
fail("Thrown " + InputManipulationException.class.getName() + " expected");
} catch (final InputManipulationException e) {
assertTrue("Limit expected in message", e.getMessage().contains("exceeds 5 seconds"));
}
}

public void testDoSAttackWithLinkedHashSet() {
final Set set = new LinkedHashSet();
Set s1 = set;
Set s2 = new LinkedHashSet();
for (int i = 0; i < 30; i++) {
final Set t1 = new LinkedHashSet();
final Set t2 = new LinkedHashSet();
t1.add("a");
t2.add("b");
s1.add(t1);
s1.add(t2);
s2.add(t2);
s2.add(t1);
s1 = t1;
s2 = t2;
}

xstream.setCollectionUpdateLimit(5);
final String xml = xstream.toXML(set);
try {
xstream.fromXML(xml);
fail("Thrown " + InputManipulationException.class.getName() + " expected");
} catch (final InputManipulationException e) {
assertTrue("Limit expected in message", e.getMessage().contains("exceeds 5 seconds"));
}
}

public void testDoSAttackWithHashMap() {
final Map map = new HashMap();
Map m1 = map;
Map m2 = new HashMap();
for (int i = 0; i < 25; i++) {
final Map t1 = new HashMap();
final Map t2 = new HashMap();
t1.put("a", "b");
t2.put("c", "d");
m1.put(t1, t2);
m1.put(t2, t1);
m2.put(t2, t1);
m2.put(t1, t2);
m1 = t1;
m2 = t2;
}
xstream.setCollectionUpdateLimit(5);

final String xml = xstream.toXML(map);
try {
xstream.fromXML(xml);
fail("Thrown " + InputManipulationException.class.getName() + " expected");
} catch (InputManipulationException e) {
assertTrue("Limit expected in message", e.getMessage().contains("exceeds 5 seconds"));
}
}

public void testDoSAttackWithLinkedHashMap() {
final Map map = new LinkedHashMap();
Map m1 = map;
Map m2 = new LinkedHashMap();
for (int i = 0; i < 25; i++) {
final Map t1 = new LinkedHashMap();
final Map t2 = new LinkedHashMap();
t1.put("a", "b");
t2.put("c", "d");
m1.put(t1, t2);
m1.put(t2, t1);
m2.put(t2, t1);
m2.put(t1, t2);
m1 = t1;
m2 = t2;
}

xstream.setCollectionUpdateLimit(5);
final String xml = xstream.toXML(map);
try {
xstream.fromXML(xml);
fail("Thrown " + InputManipulationException.class.getName() + " expected");
} catch (final InputManipulationException e) {
assertTrue("Limit expected in message", e.getMessage().contains("exceeds 5 seconds"));
}
}

public void testDoSAttackWithHashtable() {
final Map map = new Hashtable();
Map m1 = map;
Map m2 = new Hashtable();
for (int i = 0; i < 100; i++) {
final Map t1 = new Hashtable();
final Map t2 = new Hashtable();
t1.put("a", "b");
t2.put("c", "d");
m1.put(t1, t2);
m1.put(t2, t1);
m2.put(t2, t1);
m2.put(t1, t2);
m1 = t1;
m2 = t2;
}

xstream.setCollectionUpdateLimit(5);
final String xml = xstream.toXML(map);
try {
xstream.fromXML(xml);
fail("Thrown " + InputManipulationException.class.getName() + " expected");
} catch (final InputManipulationException e) {
assertTrue("Limit expected in message", e.getMessage().contains("exceeds 5 seconds"));
}
}
}