| /* ==================================================================== |
| 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 |
| // see https://stackoverflow.com/questions/12455602/is-documentbuilder-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); |
| 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); |
| trySet(factory::setAttribute, ACCESS_EXTERNAL_DTD, ""); |
| trySet(factory::setAttribute, ACCESS_EXTERNAL_STYLESHEET, ""); |
| trySet(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 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("")); |
| } |
| } |