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

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

package edu.hm.hafner.util;

import java.io.IOException;
import java.io.Reader;
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);

            SAXParser 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 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) {
        return new InputSource(new ReaderInputStream(reader, charset));
    }

    /**
     * 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 {
        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(String.format(messageFormat, 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, String.format(messageFormat, args)), cause);
        }

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy