diff --git a/api/src/org/labkey/api/util/XmlBeansUtil.java b/api/src/org/labkey/api/util/XmlBeansUtil.java index 69cbf03e430..432a9a94202 100644 --- a/api/src/org/labkey/api/util/XmlBeansUtil.java +++ b/api/src/org/labkey/api/util/XmlBeansUtil.java @@ -15,6 +15,7 @@ */ package org.labkey.api.util; +import org.apache.logging.log4j.Logger; import org.apache.xmlbeans.XmlCursor; import org.apache.xmlbeans.XmlError; import org.apache.xmlbeans.XmlException; @@ -26,19 +27,26 @@ import org.labkey.api.portal.ProjectUrls; import org.labkey.api.security.User; import org.labkey.api.settings.LookAndFeelProperties; +import org.labkey.api.util.logging.LogHelper; import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import javax.xml.stream.XMLInputFactory; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; import java.util.Collection; import java.util.Date; import java.util.LinkedList; public class XmlBeansUtil { + private static final Logger LOG = LogHelper.getLogger(XmlBeansUtil.class, "XML schema and validator XXE hardening"); + private XmlBeansUtil() { } @@ -197,4 +205,69 @@ private static DocumentBuilderFactory documentBuilderFactory(boolean allowDocTyp result.setExpandEntityReferences(false); return result; } + + /** + * A {@link SchemaFactory} hardened against XXE (CWE-611): external DTD access blocked and external + * schema access limited to local protocols, secure processing on. Not thread-safe, so a fresh + * instance is returned per call. + */ + public static SchemaFactory schemaFactory() + { + //noinspection SchemaFactory + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + require(() -> factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)); + attempt(() -> factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""), XMLConstants.ACCESS_EXTERNAL_DTD); + // Bundled schemas compose sibling XSDs via import/include; permit local file/jar resolution while blocking network (http/https/ftp) access + attempt(() -> factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file,jar"), XMLConstants.ACCESS_EXTERNAL_SCHEMA); + return factory; + } + + // The Validator resolves entities in the instance document independently of the SchemaFactory, so it must be locked down separately. + public static Validator hardenValidator(Validator validator) + { + require(() -> validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)); + // Standalone Apache Xerces ignores the accessExternal* properties below, so these SAX features (which it does honor) are what actually block instance-document XXE on the server; the JDK factory relies on the properties instead. + attempt(() -> validator.setFeature("http://xml.org/sax/features/external-general-entities", false), "external-general-entities"); + attempt(() -> validator.setFeature("http://xml.org/sax/features/external-parameter-entities", false), "external-parameter-entities"); + attempt(() -> validator.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false), "load-external-dtd"); + attempt(() -> validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""), XMLConstants.ACCESS_EXTERNAL_DTD); + attempt(() -> validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""), XMLConstants.ACCESS_EXTERNAL_SCHEMA); + return validator; + } + + @FunctionalInterface + private interface XmlSetting + { + void apply() throws SAXException; + } + + // FEATURE_SECURE_PROCESSING is honored by every JAXP implementation, so failure to set it is fatal. + private static void require(XmlSetting setting) + { + try + { + setting.apply(); + } + catch (SAXException e) + { + throw UnexpectedException.wrap(e); + } + } + + // Implementations recognize different subsets of the XXE controls (e.g. standalone Apache Xerces rejects the JAXP 1.5 accessExternal* properties, XERCESJ-1654), so each is attempted independently and a not-recognized setting is skipped rather than aborting the rest. + private static void attempt(XmlSetting setting, String name) + { + try + { + setting.apply(); + } + catch (SAXNotRecognizedException | SAXNotSupportedException e) + { + LOG.debug("XML implementation does not recognize {}; relying on the other hardening settings", name); + } + catch (SAXException e) + { + throw UnexpectedException.wrap(e); + } + } }