All Downloads are FREE. Search and download functionalities are using the official Maven repository.

edu.hm.hafner.util.SecureXmlParserFactory Maven / Gradle / Ivy

Go to download

Provides all necessary resources for a Java project to enforce the coding style that I am using in my lectures about software development at Munich University of Applied Sciences and in all of my open source projects. It configures several static analysis tools for Maven and IntelliJ. Moreover, it provides some sample classes that already use this style guide. This classes can be used as such but are not required in this project. These classes also use some additional libraries that are included using the Maven dependency mechanism. If the sample classes are deleted then the dependencies can be safely deleted, too.

There is a newer version: 4.13.0
Show newest version
package edu.hm.hafner.util;

import java.io.IOException;
import java.io.Reader;
import java.io.Serial;
import java.nio.charset.Charset;
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.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;

import org.apache.commons.io.input.ReaderInputStream;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.helpers.DefaultHandler;

import com.google.errorprone.annotations.FormatMethod;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import static javax.xml.XMLConstants.*;

/**
 * Factory for XML Parsers that prevent XML External Entity attacks. Those attacks occur when untrusted XML input
 * containing a reference to an external entity is processed by a weakly configured XML parser.
 *
 * @author Ullrich Hafner
 * @see XML
 *         External Entity Prevention Cheat Sheet
 * @see XML parsers should not be vulnerable to XXE
 *         attacks
 */
public class SecureXmlParserFactory {
    /**
     * The following constants are copied from the Xerces distribution 2.12.2. This avoids adding a dependency to
     * Xerces.
     */
    private static final String SAX_FEATURE_PREFIX = "http://xml.org/sax/features/";
    private static final String XERCES_FEATURE_PREFIX = "http://apache.org/xml/features/";
    private static final String EXTERNAL_GENERAL_ENTITIES_FEATURE = "external-general-entities";
    private static final String EXTERNAL_PARAMETER_ENTITIES_FEATURE = "external-parameter-entities";
    private static final String RESOLVE_DTD_URIS_FEATURE = "resolve-dtd-uris";
    private static final String USE_ENTITY_RESOLVER2_FEATURE = "use-entity-resolver2";
    private static final String CREATE_ENTITY_REF_NODES_FEATURE = "dom/create-entity-ref-nodes";
    private static final String LOAD_DTD_GRAMMAR_FEATURE = "nonvalidating/load-dtd-grammar";
    private static final String LOAD_EXTERNAL_DTD_FEATURE = "nonvalidating/load-external-dtd";

    private static final String[] ENABLED_PROPERTIES = {
//            XERCES_FEATURE_PREFIX + DISALLOW_DOCTYPE_DECL_FEATURE,   - If this feature is activated we cannot parse any XML documents that use a DOCTYPE anymore
            FEATURE_SECURE_PROCESSING
    };
    private static final String[] DISABLED_PROPERTIES = {
            SAX_FEATURE_PREFIX + EXTERNAL_GENERAL_ENTITIES_FEATURE,
            SAX_FEATURE_PREFIX + EXTERNAL_PARAMETER_ENTITIES_FEATURE,
            SAX_FEATURE_PREFIX + RESOLVE_DTD_URIS_FEATURE,
            SAX_FEATURE_PREFIX + USE_ENTITY_RESOLVER2_FEATURE,
            XERCES_FEATURE_PREFIX + CREATE_ENTITY_REF_NODES_FEATURE,
            XERCES_FEATURE_PREFIX + LOAD_DTD_GRAMMAR_FEATURE,
            XERCES_FEATURE_PREFIX + LOAD_EXTERNAL_DTD_FEATURE
    };
    private static final String[] DISABLED_ATTRIBUTES = {
            ACCESS_EXTERNAL_DTD,
            ACCESS_EXTERNAL_SCHEMA,
            ACCESS_EXTERNAL_STYLESHEET
    };
    private static final String CLEAR_ATTRIBUTE = "";
    private static final String SUPPORTING_EXTERNAL_ENTITIES = "javax.xml.stream.isSupportingExternalEntities";

    /**
     * Creates a new instance of a {@link DocumentBuilder} that does not resolve external entities.
     *
     * @return a new instance of a {@link DocumentBuilder}
     */
    public DocumentBuilder createDocumentBuilder() {
        try {
            var factory = createDocumentBuilderFactory();
            factory.setXIncludeAware(false);
            factory.setExpandEntityReferences(false);
            factory.setFeature(FEATURE_SECURE_PROCESSING, true);
            setFeatures(factory);
            clearAttributes(factory);

            return factory.newDocumentBuilder();
        }
        catch (ParserConfigurationException exception) {
            throw new IllegalArgumentException("Can't create instance of DocumentBuilder", exception);
        }
    }

    @VisibleForTesting
    DocumentBuilderFactory createDocumentBuilderFactory() {
        return DocumentBuilderFactory.newInstance();
    }

    private void setFeatures(final DocumentBuilderFactory factory) {
        for (String enabledProperty : ENABLED_PROPERTIES) {
            setFeature(factory, enabledProperty, true);
        }
        for (String disabledProperty : DISABLED_PROPERTIES) {
            setFeature(factory, disabledProperty, false);
        }
    }

    private void setFeature(final DocumentBuilderFactory factory, final String enabledProperty, final boolean value) {
        try {
            factory.setFeature(enabledProperty, value);
        }
        catch (ParserConfigurationException ignored) {
            // ignore and continue
        }
    }

    private void clearAttributes(final DocumentBuilderFactory factory) {
        for (String securityAttribute : DISABLED_ATTRIBUTES) {
            try {
                factory.setAttribute(securityAttribute, CLEAR_ATTRIBUTE);
            }
            catch (IllegalArgumentException e) {
                // ignore and continue
            }
        }
    }

    private void clearAttributes(final TransformerFactory transformerFactory) {
        for (String securityAttribute : DISABLED_ATTRIBUTES) {
            try {
                transformerFactory.setAttribute(securityAttribute, CLEAR_ATTRIBUTE);
            }
            catch (IllegalArgumentException e) {
                // ignore and continue
            }
        }
    }

    /**
     * Creates a new instance of a {@link SAXParser} that does not resolve external entities.
     *
     * @return a new instance of a {@link SAXParser}
     */
    public SAXParser createSaxParser() {
        try {
            var factory = createSaxParserFactory();
            configureSaxParserFactory(factory);

            var parser = factory.newSAXParser();
            secureParser(parser);
            return parser;
        }
        catch (ParserConfigurationException | SAXException exception) {
            throw new IllegalArgumentException("Can't create instance of SAXParser", exception);
        }
    }

    @VisibleForTesting
    SAXParserFactory createSaxParserFactory() {
        return SAXParserFactory.newInstance();
    }

    /**
     * Secure the {@link SAXParser} so that it does not resolve external entities.
     *
     * @param parser
     *         the parser to secure
     */
    private void secureParser(final SAXParser parser) {
        for (String securityAttribute : DISABLED_ATTRIBUTES) {
            try {
                parser.setProperty(securityAttribute, CLEAR_ATTRIBUTE);
            }
            catch (SAXNotRecognizedException | SAXNotSupportedException e) {
                // ignore and continue
            }
        }
    }

    /**
     * Configures a {@link SAXParserFactory} so that it does not resolve external entities.
     *
     * @param factory
     *         the facotry to configure
     */
    public void configureSaxParserFactory(final SAXParserFactory factory) {
        factory.setValidating(false);
        factory.setXIncludeAware(false);

        for (String enabledProperty : ENABLED_PROPERTIES) {
            try {
                factory.setFeature(enabledProperty, true);
            }
            catch (ParserConfigurationException | SAXException ignored) {
                // ignore and continue
            }
        }
        for (String disabledProperty : DISABLED_PROPERTIES) {
            try {
                factory.setFeature(disabledProperty, false);
            }
            catch (ParserConfigurationException | SAXException ignored) {
                // ignore and continue
            }
        }
    }

    /**
     * Creates a new instance of a {@link XMLStreamReader} that does not resolve external entities.
     *
     * @param reader
     *         the reader to wrap
     *
     * @return a new instance of a {@link XMLStreamReader}
     */
    @SuppressFBWarnings(value = "XXE_XMLSTREAMREADER", justification = "The reader is secured in the called method")
    public XMLStreamReader createXmlStreamReader(final Reader reader) {
        try {
            return createSecureInputFactory().createXMLStreamReader(reader);
        }
        catch (XMLStreamException exception) {
            throw new IllegalArgumentException("Can't create instance of XMLStreamReader", exception);
        }
    }

    /**
     * Creates a new instance of a {@link XMLStreamReader} that does not resolve external entities.
     *
     * @param reader
     *         the reader to wrap
     *
     * @return a new instance of a {@link XMLStreamReader}
     */
    @SuppressFBWarnings(value = "XXE_XMLSTREAMREADER", justification = "The reader is secured in the called method")
    public XMLEventReader createXmlEventReader(final Reader reader) {
        try {
            return createSecureInputFactory().createXMLEventReader(reader);
        }
        catch (XMLStreamException exception) {
            throw new IllegalArgumentException("Can't create instance of XMLEventReader", exception);
        }
    }

    private XMLInputFactory createSecureInputFactory() {
        var factory = createXmlInputFactory();
        factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
        factory.setProperty(SUPPORTING_EXTERNAL_ENTITIES, false);
        return factory;
    }

    @VisibleForTesting
    XMLInputFactory createXmlInputFactory() {
        return XMLInputFactory.newInstance();
    }

    /**
     * Creates a {@link SAXParser} that does not resolve external entities and parses the provided content with the
     * given SAX {@link DefaultHandler}.
     *
     * @param reader
     *         the content that should be parsed
     * @param charset
     *         the charset to use when reading the content
     * @param handler
     *         the SAX handler to parse the file
     *
     * @throws ParsingException
     *         if the file could not be parsed
     */
    @SuppressFBWarnings(value = "XXE_SAXPARSER", justification = "The parser is secured in the called method")
    public void parse(final Reader reader, final Charset charset, final DefaultHandler handler) {
        try {
            createSaxParser().parse(createInputSource(reader, charset), handler);
        }
        catch (SAXException | IOException exception) {
            throw new ParsingException(exception);
        }
    }

    /**
     * Parses the provided content into a {@link Document}.
     *
     * @param reader
     *         the content that should be parsed
     * @param charset
     *         the charset to use when reading the content
     *
     * @return the file content as a document
     * @throws ParsingException
     *         if the file could not be parsed
     */
    @SuppressFBWarnings(value = "XXE_DOCUMENT", justification = "The parser is secured in the called method")
    public Document readDocument(final Reader reader, final Charset charset) {
        try {
            return createDocumentBuilder().parse(createInputSource(reader, charset));
        }
        catch (SAXException | IOException exception) {
            throw new ParsingException(exception);
        }
    }

    private InputSource createInputSource(final Reader reader, final Charset charset) throws IOException {
        var inputStream = ReaderInputStream.builder().setReader(reader).setCharset(charset).get();

        return new InputSource(inputStream);
    }

    /**
     * Creates a {@link Transformer} that does not resolve external entities and stylesheets.
     *
     * @return the created {@link Transformer}
     */
    @SuppressFBWarnings(value = {"XXE_DTD_TRANSFORM_FACTORY", "XXE_XSLT_TRANSFORM_FACTORY"}, justification = "The transformer is secured in the called method")
    public Transformer createTransformer() {
        try {
            var transformerFactory = createTransformerFactory();

            clearAttributes(transformerFactory);

            return transformerFactory.newTransformer();
        }
        catch (TransformerConfigurationException exception) {
            throw new IllegalArgumentException("Can't create instance of Transformer", exception);
        }
    }

    @VisibleForTesting
    @SuppressFBWarnings(value = {"XXE_DTD_TRANSFORM_FACTORY", "XXE_XSLT_TRANSFORM_FACTORY"}, justification = "The transformer is secured in the called method")
    TransformerFactory createTransformerFactory() {
        return TransformerFactory.newInstance();
    }

    /**
     * Indicates that during parsing a non-recoverable error has been occurred.
     */
    public static class ParsingException extends RuntimeException {
        @Serial
        private static final long serialVersionUID = -9016364685084958944L;

        /**
         * Constructs a new {@link ParsingException} with the specified cause.
         *
         * @param cause
         *         the cause (which is saved for later retrieval by the {@link #getCause()} method).
         */
        public ParsingException(final Throwable cause) {
            super(createMessage(cause, "Exception occurred during parsing"), cause);
        }

        /**
         * Constructs a new {@link ParsingException} with the specified message.
         *
         * @param messageFormat
         *         the message as a format string as described in Format string
         *         syntax
         * @param args
         *         Arguments referenced by the format specifiers in the format string.  If there are more arguments than
         *         format specifiers, the extra arguments are ignored.  The number of arguments is variable and may be zero.
         *         The maximum number of arguments is limited by the maximum dimension of a Java array as defined by
         *         The Java™ Virtual Machine Specification. The behaviour on a {@code null} argument
         *         depends on the conversion.
         */
        @FormatMethod
        public ParsingException(final String messageFormat, final Object... args) {
            super(messageFormat.formatted(args));
        }

        /**
         * Constructs a new {@link ParsingException} with the specified cause and message.
         *
         * @param cause
         *         the cause (which is saved for later retrieval by the {@link #getCause()} method).
         * @param messageFormat
         *         the message as a format string as described in Format string
         *         syntax
         * @param args
         *         Arguments referenced by the format specifiers in the format string.  If there are more arguments than
         *         format specifiers, the extra arguments are ignored.  The number of arguments is variable and may be zero.
         *         The maximum number of arguments is limited by the maximum dimension of a Java array as defined by
         *         The Java™ Virtual Machine Specification. The behaviour on a {@code null} argument
         *         depends on the conversion.
         */
        @FormatMethod
        public ParsingException(final Throwable cause, final String messageFormat, final Object... args) {
            super(createMessage(cause, messageFormat.formatted(args)), cause);
        }

        private static String createMessage(final Throwable cause, final String message) {
            return "%s%n%s%n%s".formatted(message,
                    ExceptionUtils.getMessage(cause), ExceptionUtils.getStackTrace(cause));
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy