123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- /* ====================================================================
- Licensed to the Apache Software Foundation (ASF) under one or more
- contributor license agreements. See the NOTICE file distributed with
- this work for additional information regarding copyright ownership.
- The ASF licenses this file to You under the Apache License, Version 2.0
- (the "License"); you may not use this file except in compliance with
- the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- ==================================================================== */
-
- package org.apache.poi.util;
-
- import static javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD;
- import static javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA;
- import static javax.xml.XMLConstants.ACCESS_EXTERNAL_STYLESHEET;
- import static javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING;
- import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI;
- import static javax.xml.stream.XMLInputFactory.IS_NAMESPACE_AWARE;
- import static javax.xml.stream.XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES;
- import static javax.xml.stream.XMLInputFactory.IS_VALIDATING;
- import static javax.xml.stream.XMLInputFactory.SUPPORT_DTD;
- import static javax.xml.stream.XMLOutputFactory.IS_REPAIRING_NAMESPACES;
-
- import java.io.StringReader;
- import java.lang.reflect.Method;
- import java.util.concurrent.TimeUnit;
-
- import javax.xml.parsers.DocumentBuilder;
- import javax.xml.parsers.DocumentBuilderFactory;
- import javax.xml.parsers.ParserConfigurationException;
- import javax.xml.parsers.SAXParserFactory;
- import javax.xml.stream.XMLEventFactory;
- import javax.xml.stream.XMLInputFactory;
- import javax.xml.stream.XMLOutputFactory;
- import javax.xml.transform.OutputKeys;
- import javax.xml.transform.Transformer;
- import javax.xml.transform.TransformerConfigurationException;
- import javax.xml.transform.TransformerException;
- import javax.xml.transform.TransformerFactory;
- import javax.xml.validation.SchemaFactory;
-
- import org.apache.logging.log4j.Level;
- import org.apache.logging.log4j.LogManager;
- import org.apache.logging.log4j.Logger;
- import org.xml.sax.ErrorHandler;
- import org.xml.sax.InputSource;
- import org.xml.sax.SAXException;
- import org.xml.sax.SAXParseException;
- import org.xml.sax.XMLReader;
-
- /**
- * Helper methods for working with javax.xml classes.
- *
- * @see <a href="https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html">OWASP XXE</a>
- */
- @Internal
- public final class XMLHelper {
- static final String FEATURE_LOAD_DTD_GRAMMAR = "http://apache.org/xml/features/nonvalidating/load-dtd-grammar";
- static final String FEATURE_LOAD_EXTERNAL_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
- static final String FEATURE_DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl";
- static final String FEATURE_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities";
- static final String FEATURE_EXTERNAL_ENTITIES = "http://xml.org/sax/features/external-general-entities";
- static final String PROPERTY_ENTITY_EXPANSION_LIMIT = "http://www.oracle.com/xml/jaxp/properties/entityExpansionLimit";
- static final String PROPERTY_SECURITY_MANAGER = "http://apache.org/xml/properties/security-manager";
- static final String METHOD_ENTITY_EXPANSION_XERCES = "setEntityExpansionLimit";
-
- static final String[] SECURITY_MANAGERS = {
- //"com.sun.org.apache.xerces.internal.util.SecurityManager",
- "org.apache.xerces.util.SecurityManager"
- };
-
-
- private static final Logger LOG = LogManager.getLogger(XMLHelper.class);
- private static long lastLog;
-
- // DocumentBuilderFactory.newDocumentBuilder is thread-safe
- private static final DocumentBuilderFactory documentBuilderFactory = getDocumentBuilderFactory();
-
- private static final SAXParserFactory saxFactory = getSaxParserFactory();
-
- @FunctionalInterface
- private interface SecurityFeature {
- void accept(String name, boolean value) throws ParserConfigurationException, SAXException, TransformerException;
- }
-
- @FunctionalInterface
- private interface SecurityProperty {
- void accept(String name, Object value) throws SAXException;
- }
-
- private XMLHelper() {
- }
-
- /**
- * Creates a new DocumentBuilderFactory, with sensible defaults
- */
- @SuppressWarnings({"squid:S2755"})
- public static DocumentBuilderFactory getDocumentBuilderFactory() {
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- factory.setNamespaceAware(true);
- // this doesn't appear to work, and we still need to limit
- // entity expansions to 1 in trySet(XercesSecurityManager)
- factory.setExpandEntityReferences(false);
- factory.setValidating(false);
- trySet(factory::setFeature, FEATURE_SECURE_PROCESSING, true);
- trySet(factory::setAttribute, ACCESS_EXTERNAL_SCHEMA, "");
- trySet(factory::setAttribute, ACCESS_EXTERNAL_DTD, "");
- trySet(factory::setFeature, FEATURE_EXTERNAL_ENTITIES, false);
- trySet(factory::setFeature, FEATURE_PARAMETER_ENTITIES, false);
- trySet(factory::setFeature, FEATURE_LOAD_EXTERNAL_DTD, false);
- trySet(factory::setFeature, FEATURE_LOAD_DTD_GRAMMAR, false);
- trySet(factory::setFeature, FEATURE_DISALLOW_DOCTYPE_DECL, true);
- trySet((n, b) -> factory.setXIncludeAware(b), "XIncludeAware", false);
-
- Object manager = getXercesSecurityManager();
- if (manager == null || !trySet(factory::setAttribute, PROPERTY_SECURITY_MANAGER, manager)) {
- // separate old version of Xerces not found => use the builtin way of setting the property
- // Note: when entity_expansion_limit==0, there is no limit!
- trySet(factory::setAttribute, PROPERTY_ENTITY_EXPANSION_LIMIT, 1);
- }
-
- return factory;
- }
-
- /**
- * Creates a new document builder, with sensible defaults
- *
- * @throws IllegalStateException If creating the DocumentBuilder fails, e.g.
- * due to {@link ParserConfigurationException}.
- */
- public static DocumentBuilder newDocumentBuilder() {
- try {
- DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
- documentBuilder.setEntityResolver(XMLHelper::ignoreEntity);
- documentBuilder.setErrorHandler(new DocHelperErrorHandler());
- return documentBuilder;
- } catch (ParserConfigurationException e) {
- throw new IllegalStateException("cannot create a DocumentBuilder", e);
- }
- }
-
- @SuppressWarnings("squid:S2755")
- public static SAXParserFactory getSaxParserFactory() {
- try {
- SAXParserFactory factory = SAXParserFactory.newInstance();
- factory.setValidating(false);
- factory.setNamespaceAware(true);
- trySet(factory::setFeature, FEATURE_SECURE_PROCESSING, true);
- trySet(factory::setFeature, FEATURE_LOAD_DTD_GRAMMAR, false);
- trySet(factory::setFeature, FEATURE_LOAD_EXTERNAL_DTD, false);
- trySet(factory::setFeature, FEATURE_EXTERNAL_ENTITIES, false);
- trySet(factory::setFeature, FEATURE_DISALLOW_DOCTYPE_DECL, true);
- return factory;
- } catch (RuntimeException | Error re) { // NOSONAR
- // this also catches NoClassDefFoundError, which may be due to a local class path issue
- // This may occur if the code is run inside a web container or a restricted JVM
- // See bug 61170: https://bz.apache.org/bugzilla/show_bug.cgi?id=61170
- logThrowable(re, "Failed to create SAXParserFactory", "-");
- throw re;
- } catch (Exception e) {
- logThrowable(e, "Failed to create SAXParserFactory", "-");
- throw new RuntimeException("Failed to create SAXParserFactory", e);
- }
- }
-
- /**
- * Creates a new SAX XMLReader, with sensible defaults
- */
- public static XMLReader newXMLReader() throws SAXException, ParserConfigurationException {
- XMLReader xmlReader = saxFactory.newSAXParser().getXMLReader();
- xmlReader.setEntityResolver(XMLHelper::ignoreEntity);
- trySet(xmlReader::setFeature, FEATURE_SECURE_PROCESSING, true);
- trySet(xmlReader::setFeature, FEATURE_EXTERNAL_ENTITIES, false);
- Object manager = getXercesSecurityManager();
- if (manager == null || !trySet(xmlReader::setProperty, PROPERTY_SECURITY_MANAGER, manager)) {
- // separate old version of Xerces not found => use the builtin way of setting the property
- trySet(xmlReader::setProperty, PROPERTY_ENTITY_EXPANSION_LIMIT, 1);
- }
- return xmlReader;
- }
-
- /**
- * Creates a new StAX XMLInputFactory, with sensible defaults
- */
- @SuppressWarnings({"squid:S2755"})
- public static XMLInputFactory newXMLInputFactory() {
- XMLInputFactory factory = XMLInputFactory.newInstance();
- trySet(factory::setProperty, IS_NAMESPACE_AWARE, true);
- trySet(factory::setProperty, IS_VALIDATING, false);
- trySet(factory::setProperty, SUPPORT_DTD, false);
- trySet(factory::setProperty, IS_SUPPORTING_EXTERNAL_ENTITIES, false);
- return factory;
- }
-
- /**
- * Creates a new StAX XMLOutputFactory, with sensible defaults
- */
- public static XMLOutputFactory newXMLOutputFactory() {
- XMLOutputFactory factory = XMLOutputFactory.newInstance();
- trySet(factory::setProperty, IS_REPAIRING_NAMESPACES, true);
- return factory;
- }
-
- /**
- * Creates a new StAX XMLEventFactory, with sensible defaults
- */
- public static XMLEventFactory newXMLEventFactory() {
- // this method seems safer on Android than getFactory()
- return XMLEventFactory.newInstance();
- }
-
- @SuppressWarnings({"squid:S4435","java:S2755"})
- public static TransformerFactory getTransformerFactory() {
- TransformerFactory factory = TransformerFactory.newInstance();
- trySet(factory::setFeature, FEATURE_SECURE_PROCESSING, true);
- quietSet(factory::setAttribute, ACCESS_EXTERNAL_DTD, "");
- trySet(factory::setAttribute, ACCESS_EXTERNAL_STYLESHEET, "");
- quietSet(factory::setAttribute, ACCESS_EXTERNAL_SCHEMA, "");
- return factory;
- }
-
- public static Transformer newTransformer() throws TransformerConfigurationException {
- Transformer serializer = getTransformerFactory().newTransformer();
- // TODO set encoding from a command argument
- serializer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
- serializer.setOutputProperty(OutputKeys.INDENT, "no");
- serializer.setOutputProperty(OutputKeys.METHOD, "xml");
- return serializer;
- }
-
- @SuppressWarnings("java:S2755")
- public static SchemaFactory getSchemaFactory() {
- SchemaFactory factory = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI);
- trySet(factory::setFeature, FEATURE_SECURE_PROCESSING, true);
- trySet(factory::setProperty, ACCESS_EXTERNAL_DTD, "");
- trySet(factory::setProperty, ACCESS_EXTERNAL_STYLESHEET, "");
- trySet(factory::setProperty, ACCESS_EXTERNAL_SCHEMA, "");
- return factory;
- }
-
-
- private static Object getXercesSecurityManager() {
- // Try built-in JVM one first, standalone if not
- for (String securityManagerClassName : SECURITY_MANAGERS) {
- try {
- Object mgr = Class.forName(securityManagerClassName).getDeclaredConstructor().newInstance();
- Method setLimit = mgr.getClass().getMethod(METHOD_ENTITY_EXPANSION_XERCES, Integer.TYPE);
- setLimit.invoke(mgr, 1);
- // Stop once one can be setup without error
- return mgr;
- } catch (ClassNotFoundException ignored) {
- // continue without log, this is expected in some setups
- } catch (Throwable e) { // NOSONAR - also catch things like NoClassDefError here
- logThrowable(e, "SAX Feature unsupported", securityManagerClassName);
- }
- }
-
- return null;
- }
-
- @SuppressWarnings("UnusedReturnValue")
- private static boolean trySet(SecurityFeature feature, String name, boolean value) {
- try {
- feature.accept(name, value);
- return true;
- } catch (Exception e) {
- logThrowable(e, "SAX Feature unsupported", name);
- } catch (Error ame) {
- logThrowable(ame, "Cannot set SAX feature because outdated XML parser in classpath", name);
- }
- return false;
- }
-
- private static boolean trySet(SecurityProperty property, String name, Object value) {
- try {
- property.accept(name, value);
- return true;
- } catch (Exception e) {
- logThrowable(e, "SAX Feature unsupported", name);
- } catch (Error ame) {
- // ignore all top error object - GraalVM in native mode is not coping with java.xml error message resources
- logThrowable(ame, "Cannot set SAX feature because outdated XML parser in classpath", name);
- }
- return false;
- }
-
- private static boolean quietSet(SecurityProperty property, String name, Object value) {
- try {
- property.accept(name, value);
- return true;
- } catch (Exception|Error e) {
- // ok to ignore
- }
- return false;
- }
-
- private static void logThrowable(Throwable t, String message, String name) {
- if (System.currentTimeMillis() > lastLog + TimeUnit.MINUTES.toMillis(5)) {
- LOG.atWarn().withThrowable(t).log("{} [log suppressed for 5 minutes]{}", message, name);
- lastLog = System.currentTimeMillis();
- }
- }
-
- private static class DocHelperErrorHandler implements ErrorHandler {
-
- public void warning(SAXParseException exception) {
- printError(Level.WARN, exception);
- }
-
- public void error(SAXParseException exception) {
- printError(Level.ERROR, exception);
- }
-
- public void fatalError(SAXParseException exception) throws SAXException {
- printError(Level.FATAL, exception);
- throw exception;
- }
-
- /**
- * Prints the error message.
- */
- private void printError(Level type, SAXParseException ex) {
- String systemId = ex.getSystemId();
- if (systemId != null) {
- int index = systemId.lastIndexOf('/');
- if (index != -1) {
- systemId = systemId.substring(index + 1);
- }
- }
- String message = (systemId == null ? "" : systemId) +
- ':' + ex.getLineNumber() +
- ':' + ex.getColumnNumber() +
- ':' + ex.getMessage();
-
- LOG.atLevel(type).withThrowable(ex).log(message);
- }
- }
-
- private static InputSource ignoreEntity(String publicId, String systemId) {
- return new InputSource(new StringReader(""));
- }
- }
|