/*************************************************************************
* *
* CESeCore: CE Security Core *
* *
* This software is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or any later version. *
* *
* See terms of license at gnu.org. *
* *
*************************************************************************/
package org.cesecore.util;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import org.cesecore.certificates.certificateprofile.CertificatePolicy;
import org.cesecore.certificates.certificateprofile.PKIDisclosureStatement;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
/**
*
Implements a subset of XMLDecoder in a secure way, without allowing arbitrary classes to be loaded or methods to be invoked.
* Only primitive types, Strings, Lists and Maps are allowed.
*
*
Currently unimplemented parts of the XML format:
*
*
Multiple references to the same object (id/idref)
*
Non-Unicode characters in strings/chars
*
Deserialization of Class objects
*
Uncommon or custom collection types
*
*
*
Differences from XMLDecoder:
*
*
The SecureXMLDecoder throws an IOException on error instead of using the ExceptionListener.
*
Throws EOFException instead of ArrayIndexOutOfBoundsException at end of file
*
*
* @version $Id: SecureXMLDecoder.java 26362 2017-08-18 09:41:23Z samuellb $
*/
public class SecureXMLDecoder implements AutoCloseable {
private final InputStream is;
private final XmlPullParser parser;
private boolean seenHeader = false;
private boolean closed = false;
public SecureXMLDecoder(final InputStream is) {
this.is = is;
try {
final XmlPullParserFactory fact = XmlPullParserFactory.newInstance();
fact.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, false); // can be abused to cause exponential memory usage
fact.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
fact.setFeature(XmlPullParser.FEATURE_VALIDATION, false);
parser = fact.newPullParser();
parser.setInput(is, "UTF-8");
} catch (XmlPullParserException e) {
throw new IllegalStateException(e);
}
}
@Override
public void close() {
closed = true;
try {
is.close();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
/**
* Reads the next object from the stream, and returns it.
*
* Note: This implementation does not throw ArrayIndexOutOfBoundsException on EOF, but returns null instead.
*
* @return The deserialized object, or null when there are no more objects.
* @throws IOException On parse error or IO error.
*/
public Object readObject() throws IOException {
if (closed) {
throw new IllegalStateException("Decoder object is closed");
}
try {
if (!seenHeader) {
readHeader();
}
while (true) {
switch (parser.getEventType()) {
case XmlPullParser.START_TAG:
return readValue();
case XmlPullParser.END_TAG:
if (parser.next() != XmlPullParser.END_DOCUMENT) {
throw new IOException("Data after end of root element");
}
// NOPMD: Fall through
case XmlPullParser.END_DOCUMENT:
throw new EOFException("Reached end of XML document");
default:
throw new IllegalStateException("Got invalid/unsupported XML event type");
}
}
} catch (XmlPullParserException e) {
throw new IOException(e);
}
}
/** Reads the <java version="xx" class="xx"> header */
private void readHeader() throws XmlPullParserException, IOException {
if (parser.getEventType() != XmlPullParser.START_DOCUMENT) {
throw new IOException("Incorrect header of XML document");
}
if (parser.nextTag() != XmlPullParser.START_TAG) {
throw new IOException("Expected a root element");
}
if (!"java".equals(parser.getName())) {
throw new IOException("Expected root element");
}
final String className = parser.getAttributeValue(null, "class");
if (!"java.beans.XMLDecoder".equals(className)) {
throw new IOException("Unsupported decoder class. Only \"java.beans.XMLDecoder\" is supported");
}
parser.nextTag();
seenHeader = true;
}
/** Reads an object, array or (boxed) elementary type value. */
private Object readValue() throws XmlPullParserException, IOException {
return readValue(true);
}
/** Reads an object, array or (boxed) elementary type value. */
private Object readValue(boolean disallowTextAfterElement) throws XmlPullParserException, IOException {
final String tag = parser.getName();
final Object value;
// Read the element contents depending on the type
switch (tag) {
case "string":
value = readString();
break;
case "boolean":
value = Boolean.valueOf(readText());
break;
case "char":
final String charCode = parser.getAttributeValue(null, "code");
final String charValue = readText();
if (charCode != null) {
value = (char) Integer.parseInt(charCode.substring(1));
} else if (charValue.length() == 1) {
value = charValue.charAt(0);
} else {
throw new IOException(errorMessage("Invalid length of value, and no \"code\" attribute present."));
}
break;
case "byte":
value = Byte.valueOf(readText());
break;
case "short":
value = Short.valueOf(readText());
break;
case "int":
value = Integer.valueOf(readText());
break;
case "long":
value = Long.valueOf(readText());
break;
case "float":
value = Float.valueOf(readText());
break;
case "double":
value = Double.valueOf(readText());
break;
case "null":
value = null;
parser.nextTag();
break;
case "class":
try {
//Only allow classes from our own hierarchy
String className = readText();
if(!(className.startsWith("org.ejbca") || className.startsWith("org.cesecore"))) {
throw new IOException("Unauthorized class was decoded from XML: " + className);
}
value = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new IOException("Unknown class was sent with import.", e);
}
break;
case "object":
final String className = parser.getAttributeValue(null, "class");
String method = parser.getAttributeValue(null, "method"); // used from java.util.Collections
parser.nextTag();
// If we need to support a lot of more classes here (or custom classes), we could instead load the
// classes dynamically with Class.forName (after checking the name whitelist). Then we could check
// which interface the class implements (Collection or Map) and use the appropriate parse method.
switch (className) {
case "java.util.ArrayList": {
List