package jenkins.util.xml;
import jenkins.util.SystemProperties;
import org.apache.commons.io.IOUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
/**
* Utilities useful when working with various XML types.
*/
@Restricted(NoExternalUse.class)
public final class XMLUtils {
private final static Logger LOGGER = LogManager.getLogManager().getLogger(XMLUtils.class.getName());
private final static String DISABLED_PROPERTY_NAME = XMLUtils.class.getName() + ".disableXXEPrevention";
private static final String FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities";
private static final String FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities";
/**
* Transform the source to the output in a manner that is protected against XXE attacks.
* If the transform can not be completed safely then an IOException is thrown.
* Note - to turn off safety set the system property <code>disableXXEPrevention</code> to <code>true</code>.
* @param source The XML input to transform. - This should be a <code>StreamSource</code> or a
* <code>SAXSource</code> in order to be able to prevent XXE attacks.
* @param out The Result of transforming the <code>source</code>.
*/
public static void safeTransform(@Nonnull Source source, @Nonnull Result out) throws TransformerException,
SAXException {
InputSource src = SAXSource.sourceToInputSource(source);
if (src != null) {
SAXTransformerFactory stFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
stFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
try {
xmlReader.setFeature(FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_GENERAL_ENTITIES, false);
}
catch (SAXException ignored) { /* ignored */ }
try {
xmlReader.setFeature(FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_PARAMETER_ENTITIES, false);
}
catch (SAXException ignored) { /* ignored */ }
// defend against XXE
// the above features should strip out entities - however the feature may not be supported depending
// on the xml implementation used and this is out of our control.
// So add a fallback plan if all else fails.
xmlReader.setEntityResolver(RestrictiveEntityResolver.INSTANCE);
SAXSource saxSource = new SAXSource(xmlReader, src);
_transform(saxSource, out);
}
else {
// for some reason we could not convert source
// this applies to DOMSource and StAXSource - and possibly 3rd party implementations...
// a DOMSource can already be compromised as it is parsed by the time it gets to us.
if (SystemProperties.getBoolean(DISABLED_PROPERTY_NAME)) {
LOGGER.log(Level.WARNING, "XML external entity (XXE) prevention has been disabled by the system " +
"property {0}=true Your system may be vulnerable to XXE attacks.", DISABLED_PROPERTY_NAME);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Caller stack trace: ", new Exception("XXE Prevention caller history"));
}
_transform(source, out);
}
else {
throw new TransformerException("Could not convert source of type " + source.getClass() + " and " +
"XXEPrevention is enabled.");
}
}
}
/**
* Parse the supplied XML stream data to a {@link Document}.
* <p>
* This function does not close the stream.
*
* @param stream The XML stream.
* @return The XML {@link Document}.
* @throws SAXException Error parsing the XML stream data e.g. badly formed XML.
* @throws IOException Error reading from the steam.
* @since 2.0
*/
public static @Nonnull Document parse(@Nonnull Reader stream) throws SAXException, IOException {
DocumentBuilder docBuilder;
try {
docBuilder = newDocumentBuilderFactory().newDocumentBuilder();
docBuilder.setEntityResolver(RestrictiveEntityResolver.INSTANCE);
} catch (ParserConfigurationException e) {
throw new IllegalStateException("Unexpected error creating DocumentBuilder.", e);
}
return docBuilder.parse(new InputSource(stream));
}
/**
* Parse the supplied XML file data to a {@link Document}.
* @param file The file to parse.
* @param encoding The encoding of the XML in the file.
* @return The parsed document.
* @throws SAXException Error parsing the XML file data e.g. badly formed XML.
* @throws IOException Error reading from the file.
* @since 2.0
*/
public static @Nonnull Document parse(@Nonnull File file, @Nonnull String encoding) throws SAXException, IOException {
if (!file.exists() || !file.isFile()) {
throw new IllegalArgumentException(String.format("File %s does not exist or is not a 'normal' file.", file.getAbsolutePath()));
}
FileInputStream fileInputStream = new FileInputStream(file);
try {
InputStreamReader fileReader = new InputStreamReader(fileInputStream, encoding);
try {
return parse(fileReader);
} finally {
IOUtils.closeQuietly(fileReader);
}
} finally {
IOUtils.closeQuietly(fileInputStream);
}
}
/**
* The a "value" from an XML file using XPath.
* <p>
* Uses the system encoding for reading the file.
*
* @param xpath The XPath expression to select the value.
* @param file The file to read.
* @return The data value. An empty {@link String} is returned when the expression does not evaluate
* to anything in the document.
* @throws IOException Error reading from the file.
* @throws SAXException Error parsing the XML file data e.g. badly formed XML.
* @throws XPathExpressionException Invalid XPath expression.
* @since 2.0
*/
public static @Nonnull String getValue(@Nonnull String xpath, @Nonnull File file) throws IOException, SAXException, XPathExpressionException {
return getValue(xpath, file, Charset.defaultCharset().toString());
}
/**
* The a "value" from an XML file using XPath.
* @param xpath The XPath expression to select the value.
* @param file The file to read.
* @param fileDataEncoding The file data format.
* @return The data value. An empty {@link String} is returned when the expression does not evaluate
* to anything in the document.
* @throws IOException Error reading from the file.
* @throws SAXException Error parsing the XML file data e.g. badly formed XML.
* @throws XPathExpressionException Invalid XPath expression.
* @since 2.0
*/
public static @Nonnull String getValue(@Nonnull String xpath, @Nonnull File file, @Nonnull String fileDataEncoding) throws IOException, SAXException, XPathExpressionException {
Document document = parse(file, fileDataEncoding);
return getValue(xpath, document);
}
/**
* The a "value" from an XML file using XPath.
* @param xpath The XPath expression to select the value.
* @param document The document from which the value is to be extracted.
* @return The data value. An empty {@link String} is returned when the expression does not evaluate
* to anything in the document.
* @throws XPathExpressionException Invalid XPath expression.
* @since 2.0
*/
public static String getValue(String xpath, Document document) throws XPathExpressionException {
XPath xPathProcessor = XPathFactory.newInstance().newXPath();
return xPathProcessor.compile(xpath).evaluate(document);
}
/**
* potentially unsafe XML transformation.
* @param source The XML input to transform.
* @param out The Result of transforming the <code>source</code>.
*/
private static void _transform(Source source, Result out) throws TransformerException {
TransformerFactory factory = TransformerFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
// this allows us to use UTF-8 for storing data,
// plus it checks any well-formedness issue in the submitted data.
Transformer t = factory.newTransformer();
t.transform(source, out);
}
private static DocumentBuilderFactory newDocumentBuilderFactory() {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
// Set parser features to prevent against XXE etc.
// Note: setting only the external entity features on DocumentBuilderFactory instance
// (ala how safeTransform does it for SAXTransformerFactory) does seem to work (was still
// processing the entities - tried Oracle JDK 7 and 8 on OSX). Setting seems a bit extreme,
// but looks like there's no other choice.
documentBuilderFactory.setXIncludeAware(false);
documentBuilderFactory.setExpandEntityReferences(false);
setDocumentBuilderFactoryFeature(documentBuilderFactory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
setDocumentBuilderFactoryFeature(documentBuilderFactory, FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_GENERAL_ENTITIES, false);
setDocumentBuilderFactoryFeature(documentBuilderFactory, FEATURE_HTTP_XML_ORG_SAX_FEATURES_EXTERNAL_PARAMETER_ENTITIES, false);
setDocumentBuilderFactoryFeature(documentBuilderFactory, "http://apache.org/xml/features/disallow-doctype-decl", true);
return documentBuilderFactory;
}
private static void setDocumentBuilderFactoryFeature(DocumentBuilderFactory documentBuilderFactory, String feature, boolean state) {
try {
documentBuilderFactory.setFeature(feature, state);
} catch (Exception e) {}
}
}