diff --git a/Kitodo-API/src/main/java/org/kitodo/api/externaldatamanagement/XmlResponseHandler.java b/Kitodo-API/src/main/java/org/kitodo/api/externaldatamanagement/XmlResponseHandler.java index 50dece0066c..2b34ba41307 100644 --- a/Kitodo-API/src/main/java/org/kitodo/api/externaldatamanagement/XmlResponseHandler.java +++ b/Kitodo-API/src/main/java/org/kitodo/api/externaldatamanagement/XmlResponseHandler.java @@ -13,7 +13,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; -import java.lang.reflect.UndeclaredThrowableException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.LinkedList; @@ -34,6 +33,7 @@ import org.kitodo.exceptions.CatalogException; import org.kitodo.exceptions.ConfigException; import org.kitodo.exceptions.NoRecordFoundException; +import org.kitodo.utils.XMLSecurity; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; @@ -42,17 +42,17 @@ public class XmlResponseHandler { - private static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + private static final DocumentBuilderFactory documentBuilderFactory; private static final XMLOutputter xmlOutputter = new XMLOutputter(); private static final XPath xPath = XPathFactory.newInstance().newXPath(); static { - documentBuilderFactory.setNamespaceAware(true); try { - documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - } catch (ParserConfigurationException parserConfigurationException) { - throw new UndeclaredThrowableException(parserConfigurationException); + documentBuilderFactory = XMLSecurity.newDocumentBuilderFactory(); + } catch (ParserConfigurationException e) { + throw new ExceptionInInitializerError(e); } + documentBuilderFactory.setNamespaceAware(true); xmlOutputter.setFormat(Format.getPrettyFormat()); } diff --git a/Kitodo-API/src/main/java/org/kitodo/utils/XMLSecurity.java b/Kitodo-API/src/main/java/org/kitodo/utils/XMLSecurity.java new file mode 100644 index 00000000000..7f11670afd1 --- /dev/null +++ b/Kitodo-API/src/main/java/org/kitodo/utils/XMLSecurity.java @@ -0,0 +1,155 @@ +/* + * (c) Kitodo. Key to digital objects e. V. + * + * This file is part of the Kitodo project. + * + * It is licensed under GNU General Public License version 3 or later. + * + * For the full copyright and license information, please read the + * GPL3-License.txt file that was distributed with this source code. + */ + +package org.kitodo.utils; + +import java.io.InputStream; + +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.transform.TransformerFactory; +import javax.xml.transform.sax.SAXSource; +import javax.xml.validation.SchemaFactory; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; +import org.xml.sax.XMLReader; + +/** + * Provides factory instances that are hardened against XML External Entity + * (XXE) injection and unrestricted document type definitions. + */ +public final class XMLSecurity { + + private static final Logger logger = LogManager.getLogger(XMLSecurity.class); + + private static final String DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl"; + private static final String EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities"; + private static final String EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities"; + + private XMLSecurity() { + } + + /** + * Create and return a DocumentBuilderFactory that rejects DOCTYPE declarations and + * external entity resolution. + * + * @return hardened DocumentBuilderFactory + * @throws ParserConfigurationException if a feature cannot be set + */ + public static DocumentBuilderFactory newDocumentBuilderFactory() throws ParserConfigurationException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature(DISALLOW_DOCTYPE_DECL, true); + factory.setFeature(EXTERNAL_GENERAL_ENTITIES, false); + factory.setFeature(EXTERNAL_PARAMETER_ENTITIES, false); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + return factory; + } + + /** + * Create and return a TransformerFactory that restricts access to external DTDs + * and stylesheets to prevent XML External Entity (XXE) injection. + * + * @return hardened TransformerFactory + */ + public static TransformerFactory newTransformerFactory() { + TransformerFactory factory = TransformerFactory.newInstance(); + try { + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + } catch (IllegalArgumentException e) { + logger.warn("Unable to restrict external access on TransformerFactory '{}': {}", + factory.getClass().getName(), e.getMessage()); + } + return factory; + } + + /** + * Create and return a SchemaFactory that rejects external DTD access to prevent + * XML External Entity (XXE) injection during XML validation. + * + * @return hardened SchemaFactory + */ + public static SchemaFactory newSchemaFactory() { + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + try { + factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + } catch (IllegalArgumentException | SAXNotRecognizedException | SAXNotSupportedException e) { + logger.warn("Unable to restrict external access on SchemaFactory '{}': {}", + factory.getClass().getName(), e.getMessage()); + } + return factory; + } + + /** + * Create and return an XMLInputFactory with DTD support and external entity + * resolution disabled to prevent XML External Entity (XXE) injection. + * + * @return hardened XMLInputFactory + */ + public static XMLInputFactory newXmlInputFactory() { + XMLInputFactory factory = XMLInputFactory.newInstance(); + factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + return factory; + } + + /** + * Create and return a SAXParserFactory that rejects DOCTYPE declarations and + * external entity resolution to prevent XML External Entity (XXE) injection. + * + * @return hardened SAXParserFactory + */ + public static SAXParserFactory newSaxParserFactory() { + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setValidating(false); + factory.setNamespaceAware(true); + try { + factory.setFeature(DISALLOW_DOCTYPE_DECL, true); + factory.setFeature(EXTERNAL_GENERAL_ENTITIES, false); + factory.setFeature(EXTERNAL_PARAMETER_ENTITIES, false); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + } catch (ParserConfigurationException | SAXNotRecognizedException | SAXNotSupportedException e) { + throw new IllegalStateException("Unable to harden SAXParserFactory", e); + } + return factory; + } + + /** + * Create and return a SAXSource that rejects DOCTYPE declarations and external + * entity resolution to prevent XML External Entity (XXE) injection during + * transformation of the given input stream. + * + * @param inputStream input stream containing the XML document to transform + * @return hardened SAXSource + * @throws ParserConfigurationException if a feature cannot be set + * @throws SAXException if the SAX parser cannot be created + */ + public static SAXSource newSecureSource(InputStream inputStream) throws ParserConfigurationException, SAXException { + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setFeature(DISALLOW_DOCTYPE_DECL, true); + factory.setFeature(EXTERNAL_GENERAL_ENTITIES, false); + factory.setFeature(EXTERNAL_PARAMETER_ENTITIES, false); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + XMLReader reader = factory.newSAXParser().getXMLReader(); + return new SAXSource(reader, new InputSource(inputStream)); + } +} diff --git a/Kitodo-DataEditor/src/main/java/org/kitodo/dataeditor/JaxbXmlUtils.java b/Kitodo-DataEditor/src/main/java/org/kitodo/dataeditor/JaxbXmlUtils.java index af52fe5ee28..1491c02061d 100644 --- a/Kitodo-DataEditor/src/main/java/org/kitodo/dataeditor/JaxbXmlUtils.java +++ b/Kitodo-DataEditor/src/main/java/org/kitodo/dataeditor/JaxbXmlUtils.java @@ -26,9 +26,10 @@ import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; @@ -38,6 +39,8 @@ import org.kitodo.dataformat.metskitodo.KitodoType; import org.kitodo.dataformat.metskitodo.MdSecType; import org.kitodo.serviceloader.KitodoServiceLoader; +import org.kitodo.utils.XMLSecurity; +import org.xml.sax.SAXException; /** * Provides methods for handling jaxb generated java objects and xml files. @@ -61,15 +64,16 @@ private JaxbXmlUtils() { */ static String transformXmlByXslt(URI xmlFile, URI xslFile) throws TransformerException, IOException { FileManagementInterface fileManagementModule = new KitodoServiceLoader<>(FileManagementInterface.class).loadModule(); - TransformerFactory factory = TransformerFactory.newInstance(); StreamSource styleSource = new StreamSource(xslFile.getPath()); - Transformer transformer = factory.newTransformer(styleSource); + Transformer transformer = XMLSecurity.newTransformerFactory().newTransformer(styleSource); try (InputStream inputStream = fileManagementModule.read(xmlFile); StringWriter stringWriter = new StringWriter()) { - StreamSource source = new StreamSource(inputStream); + Source source = XMLSecurity.newSecureSource(inputStream); StreamResult result = new StreamResult(stringWriter); transformer.transform(source, result); return stringWriter.toString(); + } catch (ParserConfigurationException | SAXException e) { + throw new IOException(e); } } diff --git a/Kitodo-Docket/src/main/java/org/kitodo/docket/ExportDocket.java b/Kitodo-Docket/src/main/java/org/kitodo/docket/ExportDocket.java index 80ffc2fe5c8..eaa140f279b 100644 --- a/Kitodo-Docket/src/main/java/org/kitodo/docket/ExportDocket.java +++ b/Kitodo-Docket/src/main/java/org/kitodo/docket/ExportDocket.java @@ -17,10 +17,11 @@ import java.io.IOException; import java.io.OutputStream; +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.SAXResult; import javax.xml.transform.stream.StreamSource; @@ -30,6 +31,8 @@ import org.apache.fop.apps.FopFactoryBuilder; import org.apache.fop.apps.MimeConstants; import org.kitodo.api.docket.DocketData; +import org.kitodo.utils.XMLSecurity; +import org.xml.sax.SAXException; /** * This class provides generating a run note based on the generated xml log. @@ -90,7 +93,6 @@ void startExport(Iterable docketDataList, OutputStream os) throws IO private byte[] generatePdfBytes(ByteArrayOutputStream out) throws IOException { // generate pdf file - StreamSource source = new StreamSource(new ByteArrayInputStream(out.toByteArray())); StreamSource transformSource = new StreamSource(xsltFile); FopFactoryBuilder builder = new FopFactoryBuilder(new File(".").toURI()); builder.setStrictFOValidation(false); @@ -98,14 +100,19 @@ private byte[] generatePdfBytes(ByteArrayOutputStream out) throws IOException { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); // transform xml try { - Transformer xslTransformer = TransformerFactory.newInstance().newTransformer(transformSource); + Transformer xslTransformer = XMLSecurity.newTransformerFactory().newTransformer(transformSource); Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, outStream); Result res = new SAXResult(fop.getDefaultHandler()); + Source source = XMLSecurity.newSecureSource(new ByteArrayInputStream(out.toByteArray())); xslTransformer.transform(source, res); } catch (FOPException e) { throw new IOException("FOPException occurred", e); } catch (TransformerException e) { throw new IOException("TransformerException occurred", e); + } catch (ParserConfigurationException e) { + throw new IOException("ParserConfigurationException occurred", e); + } catch (SAXException e) { + throw new IOException("SAXException occurred", e); } // write the content to output stream diff --git a/Kitodo-Query-URL-Import/src/main/java/org/kitodo/queryurlimport/QueryURLImport.java b/Kitodo-Query-URL-Import/src/main/java/org/kitodo/queryurlimport/QueryURLImport.java index 912a8e3acdf..add93e7f41d 100644 --- a/Kitodo-Query-URL-Import/src/main/java/org/kitodo/queryurlimport/QueryURLImport.java +++ b/Kitodo-Query-URL-Import/src/main/java/org/kitodo/queryurlimport/QueryURLImport.java @@ -33,13 +33,10 @@ import java.util.Objects; import java.util.stream.Collectors; -import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; @@ -76,6 +73,7 @@ import org.kitodo.exceptions.CatalogException; import org.kitodo.exceptions.ConfigException; import org.kitodo.exceptions.NoRecordFoundException; +import org.kitodo.utils.XMLSecurity; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -428,16 +426,13 @@ private String createSearchFieldString(SearchInterfaceType interfaceType, Linked private Document stringToDocument(String xmlContent) throws ParserConfigurationException, IOException, SAXException { - DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + DocumentBuilder documentBuilder = XMLSecurity.newDocumentBuilderFactory().newDocumentBuilder(); return documentBuilder.parse(new InputSource(new StringReader(xmlContent))); } private String nodeToString(Node node) throws TransformerException { StringWriter writer = new StringWriter(); - Transformer transformer = TransformerFactory.newInstance().newTransformer(); + Transformer transformer = XMLSecurity.newTransformerFactory().newTransformer(); transformer.transform(new DOMSource(node), new StreamResult(writer)); return writer.toString(); } diff --git a/Kitodo-Validation/src/main/java/org/kitodo/validation/filestructure/FileStructureValidation.java b/Kitodo-Validation/src/main/java/org/kitodo/validation/filestructure/FileStructureValidation.java index 43a00b0a654..3d31ff924b1 100644 --- a/Kitodo-Validation/src/main/java/org/kitodo/validation/filestructure/FileStructureValidation.java +++ b/Kitodo-Validation/src/main/java/org/kitodo/validation/filestructure/FileStructureValidation.java @@ -32,6 +32,7 @@ import org.kitodo.api.validation.State; import org.kitodo.api.validation.ValidationResult; import org.kitodo.api.validation.filestructure.FileStructureValidationInterface; +import org.kitodo.utils.XMLSecurity; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; @@ -87,13 +88,18 @@ private ValidationResult validateStreamSource(StreamSource source, Validator val private Validator initializeXmlValidator(Collection xsdFilePaths) throws SAXException { FileStructureValidationErrorHandler xmlValidationErrorHandler = new FileStructureValidationErrorHandler(); - SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + SchemaFactory schemaFactory = XMLSecurity.newSchemaFactory(); Source[] sources = new Source[xsdFilePaths.size()]; for (int i = 0; i < xsdFilePaths.size(); i++) { sources[i] = new StreamSource(new File(xsdFilePaths.toArray(new URI[0])[i])); } Schema schema = schemaFactory.newSchema(sources); Validator xmlValidator = schema.newValidator(); + try { + xmlValidator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + } catch (IllegalArgumentException e) { + logger.warn("Unable to restrict external access on Validator: {}", e.getMessage()); + } xmlValidator.setErrorHandler(xmlValidationErrorHandler); return xmlValidator; } diff --git a/Kitodo/src/main/java/org/kitodo/export/ExportMets.java b/Kitodo/src/main/java/org/kitodo/export/ExportMets.java index 65bfae1552e..76d9c81e462 100644 --- a/Kitodo/src/main/java/org/kitodo/export/ExportMets.java +++ b/Kitodo/src/main/java/org/kitodo/export/ExportMets.java @@ -53,6 +53,7 @@ import org.kitodo.production.helper.tasks.EmptyTask; import org.kitodo.production.services.ServiceManager; import org.kitodo.production.services.file.FileService; +import org.kitodo.utils.XMLSecurity; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.SAXException; @@ -248,7 +249,7 @@ private void updateInternalLabelsIfNeeded(URI metaFile, byte[] xmlBytes, Process private Map extractLabels(byte[] xmlBytes) { Map labels = new HashMap<>(); try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilderFactory factory = XMLSecurity.newDocumentBuilderFactory(); factory.setNamespaceAware(true); Document doc = factory.newDocumentBuilder().parse(new ByteArrayInputStream(xmlBytes)); XPath xpath = XPathFactory.newInstance().newXPath(); diff --git a/Kitodo/src/main/java/org/kitodo/production/editor/XMLEditor.java b/Kitodo/src/main/java/org/kitodo/production/editor/XMLEditor.java index 163dcd28256..dea9020a9c9 100644 --- a/Kitodo/src/main/java/org/kitodo/production/editor/XMLEditor.java +++ b/Kitodo/src/main/java/org/kitodo/production/editor/XMLEditor.java @@ -22,14 +22,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; @@ -43,6 +40,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.kitodo.config.enums.KitodoConfigFile; +import org.kitodo.utils.XMLSecurity; import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -63,9 +61,7 @@ public class XMLEditor implements Serializable { */ public XMLEditor() { try { - DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - documentBuilder = documentBuilderFactory.newDocumentBuilder(); + documentBuilder = XMLSecurity.newDocumentBuilderFactory().newDocumentBuilder(); } catch (ParserConfigurationException e) { logger.error("ERROR: unable to instantiate document builder: {}", e.getMessage()); } @@ -136,8 +132,7 @@ public void saveXMLConfiguration() { logger.info("Saving configuration to file {}", currentConfigurationFile); try { Document document = documentBuilder.parse(new InputSource(new StringReader(this.xmlConfigurationString))); - TransformerFactory transformerFactory = TransformerFactory.newInstance(); - Transformer transformer = transformerFactory.newTransformer(); + Transformer transformer = XMLSecurity.newTransformerFactory().newTransformer(); DOMSource domSource = new DOMSource(document); try (FileOutputStream outputStream = new FileOutputStream(configurationFile.getFile(), false); PrintWriter printWriter = new PrintWriter(outputStream)) { diff --git a/Kitodo/src/main/java/org/kitodo/production/helper/XMLUtils.java b/Kitodo/src/main/java/org/kitodo/production/helper/XMLUtils.java index 18087448f64..16029caf06b 100644 --- a/Kitodo/src/main/java/org/kitodo/production/helper/XMLUtils.java +++ b/Kitodo/src/main/java/org/kitodo/production/helper/XMLUtils.java @@ -23,12 +23,10 @@ import java.util.NoSuchElementException; import java.util.Objects; -import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; @@ -36,7 +34,6 @@ import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.XPath; @@ -50,6 +47,7 @@ import org.kitodo.api.schemaconverter.MetadataFormat; import org.kitodo.constants.StringConstants; import org.kitodo.data.database.beans.ImportConfiguration; +import org.kitodo.utils.XMLSecurity; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -63,10 +61,6 @@ */ public class XMLUtils { - private static final String DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl"; - private static final String EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities"; - private static final String EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities"; - /** * Private constructor to hide the implicit public one. */ @@ -91,7 +85,7 @@ private XMLUtils() { public static byte[] documentToByteArray(Document data, Integer indent) throws TransformerException { ByteArrayOutputStream result = new ByteArrayOutputStream(); - Transformer transformer = TransformerFactory.newInstance().newTransformer(); + Transformer transformer = XMLSecurity.newTransformerFactory().newTransformer(); if (Objects.nonNull(indent)) { transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", indent.toString()); @@ -143,9 +137,7 @@ public static Element getFirstChildWithTagName(Node data, String tagName) { */ public static Document load(InputStream data) throws SAXException, IOException { try { - DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - return documentBuilderFactory.newDocumentBuilder().parse(data); + return XMLSecurity.newDocumentBuilderFactory().newDocumentBuilder().parse(data); } catch (ParserConfigurationException e) { throw new IOException(e.getMessage(), e); } @@ -164,9 +156,7 @@ public static Document load(InputStream data) throws SAXException, IOException { */ public static Document newDocument() throws IOException { try { - DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - return documentBuilderFactory.newDocumentBuilder().newDocument(); + return XMLSecurity.newDocumentBuilderFactory().newDocumentBuilder().newDocument(); } catch (ParserConfigurationException e) { throw new IOException(e.getMessage(), e); } @@ -184,9 +174,8 @@ public static Document newDocument() throws IOException { */ public static Document parseXMLString(String xmlString) throws IOException, ParserConfigurationException, SAXException { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilderFactory factory = XMLSecurity.newDocumentBuilderFactory(); factory.setNamespaceAware(true); - disableExternalEntities(factory); DocumentBuilder builder = factory.newDocumentBuilder(); xmlString = removeBom(xmlString); return builder.parse(new InputSource(new ByteArrayInputStream(xmlString.getBytes(StandardCharsets.UTF_8)))); @@ -250,7 +239,7 @@ public static List getElementsByTagNameAndAttributeValue(Document docum */ public static String elementToString(Element element) throws TransformerException { StringWriter stringWriter = new StringWriter(); - Transformer transformer = TransformerFactory.newInstance().newTransformer(); + Transformer transformer = XMLSecurity.newTransformerFactory().newTransformer(); transformer.transform(new DOMSource(element), new StreamResult(stringWriter)); return stringWriter.toString(); } @@ -283,9 +272,7 @@ public static DataRecord createRecordFromXMLElement(String xmlContent, ImportCon */ public static int getNumberOfEADElements(String xmlString, String eadLevel) throws XMLStreamException { int count = 0; - XMLInputFactory factory = XMLInputFactory.newInstance(); - factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); - factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + XMLInputFactory factory = XMLSecurity.newXmlInputFactory(); XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(xmlString)); while (reader.hasNext()) { int event = reader.next(); @@ -310,34 +297,11 @@ public static int getNumberOfEADElements(String xmlString, String eadLevel) thro public static void checkIfXmlIsWellFormed(String xmlContent) throws IOException, SAXException { SAXParser saxParser; try { - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - saxParserFactory.setValidating(false); - saxParserFactory.setNamespaceAware(true); - saxParserFactory.setFeature(DISALLOW_DOCTYPE_DECL, true); - - saxParser = saxParserFactory.newSAXParser(); + saxParser = XMLSecurity.newSaxParserFactory().newSAXParser(); } catch (ParserConfigurationException | SAXException e) { throw new RuntimeException(e); } InputSource inputSource = new InputSource(new StringReader(xmlContent)); saxParser.parse(inputSource, new DefaultHandler()); } - - /** - * Disable DOCTYPE declarations and external entity resolution on the given - * factory to prevent XML External Entity (XXE) injection. This mirrors the - * secure configuration already used by {@link #load(InputStream)} and the - * external-catalog response parsers. - * - * @param factory the DocumentBuilderFactory to harden - * @throws ParserConfigurationException if a feature cannot be set - */ - private static void disableExternalEntities(DocumentBuilderFactory factory) throws ParserConfigurationException { - factory.setFeature(DISALLOW_DOCTYPE_DECL, true); - factory.setFeature(EXTERNAL_GENERAL_ENTITIES, false); - factory.setFeature(EXTERNAL_PARAMETER_ENTITIES, false); - factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - factory.setXIncludeAware(false); - factory.setExpandEntityReferences(false); - } } diff --git a/Kitodo/src/main/java/org/kitodo/production/services/data/ProcessService.java b/Kitodo/src/main/java/org/kitodo/production/services/data/ProcessService.java index 9d445d63f26..d5bcd907596 100644 --- a/Kitodo/src/main/java/org/kitodo/production/services/data/ProcessService.java +++ b/Kitodo/src/main/java/org/kitodo/production/services/data/ProcessService.java @@ -127,6 +127,7 @@ import org.kitodo.production.services.workflow.WorkflowControllerService; import org.kitodo.production.workflow.KitodoNamespaceContext; import org.kitodo.serviceloader.KitodoServiceLoader; +import org.kitodo.utils.XMLSecurity; import org.primefaces.model.SortOrder; import org.primefaces.model.charts.ChartData; import org.primefaces.model.charts.axes.cartesian.linear.CartesianLinearAxes; @@ -1928,7 +1929,7 @@ public static void deleteSymlinksFromUserHomes(Task task) { */ public NodeList getNodeListFromMetadataFile(Process process, String xpath) throws IOException { try (InputStream fileInputStream = ServiceManager.getFileService().readMetadataFile(process)) { - DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilderFactory builderFactory = XMLSecurity.newDocumentBuilderFactory(); builderFactory.setNamespaceAware(true); DocumentBuilder builder = builderFactory.newDocumentBuilder(); org.w3c.dom.Document xmlDocument = builder.parse(fileInputStream); diff --git a/Kitodo/src/main/java/org/kitodo/production/services/dataformat/MetsService.java b/Kitodo/src/main/java/org/kitodo/production/services/dataformat/MetsService.java index f260d3485b2..364520306df 100644 --- a/Kitodo/src/main/java/org/kitodo/production/services/dataformat/MetsService.java +++ b/Kitodo/src/main/java/org/kitodo/production/services/dataformat/MetsService.java @@ -22,8 +22,8 @@ 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.dom.DOMSource; import javax.xml.transform.stream.StreamResult; @@ -37,6 +37,7 @@ import org.kitodo.production.helper.XMLUtils; import org.kitodo.production.services.ServiceManager; import org.kitodo.serviceloader.KitodoServiceLoader; +import org.kitodo.utils.XMLSecurity; import org.w3c.dom.Document; import org.xml.sax.SAXException; @@ -175,7 +176,11 @@ public Workpiece loadWorkpiece(Document document) throws TransformerException, I ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); Source xmlSource = new DOMSource(document); Result outputTarget = new StreamResult(outputStream); - TransformerFactory.newInstance().newTransformer().transform(xmlSource, outputTarget); + Transformer transformer = XMLSecurity.newTransformerFactory().newTransformer(); + if (Objects.isNull(transformer)) { + throw new IOException("Unable to create transformer"); + } + transformer.transform(xmlSource, outputTarget); InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); return metsXmlElementAccess.read(inputStream); } diff --git a/Kitodo/src/main/java/org/kitodo/production/thread/ImportEadProcessesThread.java b/Kitodo/src/main/java/org/kitodo/production/thread/ImportEadProcessesThread.java index 56cc839e4f6..bb8b474bd81 100644 --- a/Kitodo/src/main/java/org/kitodo/production/thread/ImportEadProcessesThread.java +++ b/Kitodo/src/main/java/org/kitodo/production/thread/ImportEadProcessesThread.java @@ -69,6 +69,7 @@ import org.kitodo.production.services.ServiceManager; import org.kitodo.production.services.data.ImportService; import org.kitodo.production.services.data.ProcessService; +import org.kitodo.utils.XMLSecurity; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; @@ -132,7 +133,7 @@ public void run() { boolean stopOnError = ConfigCore.getBooleanParameter(ParameterCore.STOP_EAD_COLLECTION_IMPORT_ON_EXCEPTION); try { int numberOfElements = XMLUtils.getNumberOfEADElements(xmlString, eadLevel); - XMLInputFactory inputFactory = XMLInputFactory.newInstance(); + XMLInputFactory inputFactory = XMLSecurity.newXmlInputFactory(); XMLEventReader eventReader = inputFactory.createXMLEventReader(new StringReader(xmlString)); boolean inProcessElement = false; boolean inParentProcessElement = false;