CVE-2022-0265 (Java, published 2022-03-03T22:15Z)
Description: value: Improper Restriction of XML External Entity Reference in GitHub repository hazelcast/hazelcast in 5.1-BETA-1.
Severity: HIGH (CVSS v3 base score: 9.8)
Weakness: CWE-611 \u2014 Improper Restriction of XML External Entity Reference
The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output.
Repository: https://github.com/hazelcast/hazelcast
Files changed:
hazelcast/src/main/java/com/hazelcast/internal/config/AbstractXmlConfigRootTagRecognizer.java
hazelcast/src/main/java/com/hazelcast/internal/util/XmlUtil.java
hazelcast/src/test/java/com/hazelcast/internal/util/XmlUtilTest.java
Commit message: Add helper method to XmlUtil to enable XXE protection in the SAXParserFactory and XMLInputFactory (#20407)



Vulnerable code:
        SAXParserFactory factory = SAXParserFactory.newInstance();
 * Utility class for XML processing.
        setFeature(dbf, "http://apache.org/xml/features/disallow-doctype-decl");
            if (Boolean.getBoolean(SYSTEM_PROPERTY_IGNORE_XXE_PROTECTION_FAILURES)) {
                LOGGER.warning("Enabling XXE protection failed. The attribute " + attributeName
                        + " is not supported by the TransformerFactory. The " + SYSTEM_PROPERTY_IGNORE_XXE_PROTECTION_FAILURES
                        + " system property is used so the XML processing continues in the UNSECURE mode"
                        + " with XXE protection disabled!!!");
            } else {
                LOGGER.severe("Enabling XXE protection failed. The attribute " + attributeName
                        + " is not supported by the TransformerFactory. This usually mean an outdated XML processor"
                        + " is present on the classpath (e.g. Xerces, Xalan). If you are not able to resolve the issue by"
                        + " fixing the classpath, the " + SYSTEM_PROPERTY_IGNORE_XXE_PROTECTION_FAILURES
                        + " system property can be used to disable XML External Entity protections."
                        + " We don't recommend disabling the XXE as such the XML processor configuration is unsecure!!!", iae);
                throw iae;
            }
            if (Boolean.getBoolean(SYSTEM_PROPERTY_IGNORE_XXE_PROTECTION_FAILURES)) {
                LOGGER.warning("Enabling XXE protection failed. The feature " + featureName
                        + " is not supported by the DocumentBuilderFactory. The " + SYSTEM_PROPERTY_IGNORE_XXE_PROTECTION_FAILURES
                        + " system property is used so the XML processing continues in the UNSECURE mode"
                        + " with XXE protection disabled!!!");
            } else {
                LOGGER.severe("Enabling XXE protection failed. The feature " + featureName
                        + " is not supported by the DocumentBuilderFactory. This usually mean an outdated XML processor"
                        + " is present on the classpath (e.g. Xerces, Xalan). If you are not able to resolve the issue by"
                        + " fixing the classpath, the " + SYSTEM_PROPERTY_IGNORE_XXE_PROTECTION_FAILURES
                        + " system property can be used to disable XML External Entity protections."
                        + " We don't recommend disabling the XXE as such the XML processor configuration is unsecure!!!", e);
                throw e;
            }
            if (Boolean.getBoolean(SYSTEM_PROPERTY_IGNORE_XXE_PROTECTION_FAILURES)) {
                LOGGER.warning("Enabling XXE protection failed. The property " + propertyName
                        + " is not supported by the SchemaFactory. The " + SYSTEM_PROPERTY_IGNORE_XXE_PROTECTION_FAILURES
                        + " system property is used so the XML processing continues in the UNSECURE mode"
                        + " with XXE protection disabled!!!");
            } else {
                LOGGER.severe("Enabling XXE protection failed. The property " + propertyName
                        + " is not supported by the SchemaFactory. This usually mean an outdated XML processor"
                        + " is present on the classpath (e.g. Xerces, Xalan). If you are not able to resolve the issue by"
                        + " fixing the classpath, the " + SYSTEM_PROPERTY_IGNORE_XXE_PROTECTION_FAILURES
                        + " system property can be used to disable XML External Entity protections."
                        + " We don't recommend disabling the XXE as such the XML processor configuration is unsecure!!!", e);
                throw e;
            }
        String xxeAttack = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "  <!DOCTYPE test [\n"
                + "    <!ENTITY xxe SYSTEM \"file:///etc/passwd\">\n" + "  ]>" + "<a><b>&xxe;</b></a>";
        assertEquals(xxeAttack, format(xxeAttack, 1));

Fixed code:
import com.hazelcast.internal.util.XmlUtil;
        SAXParserFactory factory = XmlUtil.getSAXParserFactory();
import javax.xml.parsers.SAXParserFactory;
import javax.xml.stream.XMLInputFactory;
 * Utility class for XML processing. It contains several methods to retrieve XML processing factories with XXE protection
 * enabled (based on recommendation in the
 * <a href="https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html">OWASP XXE prevention
 * cheat-sheet</a>).
    private static final String FEATURES_DISALLOW_DOCTYPE = "http://apache.org/xml/features/disallow-doctype-decl";
        setFeature(dbf, FEATURES_DISALLOW_DOCTYPE);
    /**
     * Returns {@link SAXParserFactory} with XXE protection enabled.
     */
    public static SAXParserFactory getSAXParserFactory() throws ParserConfigurationException, SAXException {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        setFeature(factory, FEATURES_DISALLOW_DOCTYPE);
        return factory;
    }

    /**
     * Returns {@link XMLInputFactory} with XXE protection enabled.
     */
    public static XMLInputFactory getXMLInputFactory() {
        XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
        setProperty(xmlInputFactory, XMLInputFactory.SUPPORT_DTD, false);
        return xmlInputFactory;
    }

            printWarningAndRethrowEventually(iae, TransformerFactory.class, "attribute " + attributeName);
            printWarningAndRethrowEventually(e, DocumentBuilderFactory.class, "feature " + featureName);
        }
    }

    static void setFeature(SAXParserFactory saxParserFactory, String featureName)
            throws ParserConfigurationException, SAXException {
        try {
            saxParserFactory.setFeature(featureName, true);
        } catch (SAXException e) {
            printWarningAndRethrowEventually(e, SAXParserFactory.class, "feature " + featureName);
        } catch (ParserConfigurationException e) {
            printWarningAndRethrowEventually(e, SAXParserFactory.class, "feature " + featureName);
            printWarningAndRethrowEventually(e, SchemaFactory.class, "property " + propertyName);
        }
    }

    static void setProperty(XMLInputFactory xmlInputFactory, String propertyName, Object value) {
        try {
            xmlInputFactory.setProperty(propertyName, value);
        } catch (IllegalArgumentException e) {
            printWarningAndRethrowEventually(e, XMLInputFactory.class, "property " + propertyName);
        }
    }

    private static <T extends Exception> void printWarningAndRethrowEventually(T cause, Class<?> clazz, String objective)
            throws T {
        String className = clazz.getSimpleName();
        if (Boolean.getBoolean(SYSTEM_PROPERTY_IGNORE_XXE_PROTECTION_FAILURES)) {
            LOGGER.warning("Enabling XXE protection failed. The " + objective + " is not supported by the " + className
                    + ". The " + SYSTEM_PROPERTY_IGNORE_XXE_PROTECTION_FAILURES
                    + " system property is used so the XML processing continues in the UNSECURE mode"
                    + " with XXE protection disabled!!!");
        } else {
            LOGGER.severe(
                    "Enabling XXE protection failed. The " + objective + " is not supported by the " + className
                            + ". This usually mean an outdated XML processor"
                            + " is present on the classpath (e.g. Xerces, Xalan). If you are not able to resolve the issue by"
                            + " fixing the classpath, the " + SYSTEM_PROPERTY_IGNORE_XXE_PROTECTION_FAILURES
                            + " system property can be used to disable XML External Entity protections."
                            + " We don't recommend disabling the XXE as such the XML processor configuration is unsecure!",
                    cause);
            throw cause;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import java.io.IOException;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.atomic.AtomicInteger;

import javax.xml.parsers.DocumentBuilder;
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 org.fusesource.hawtbuf.ByteArrayInputStream;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Before;
import org.xml.sax.HandlerBase;
    private DummyServer server;

    @Before
    public void before() throws IOException {
        server = new DummyServer();
        server.start();
    }

    @After
    public void after() {
        server.stop();
    }

    @Test
    public void testUnprotectedXxe() throws Exception {
        DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        try {
            db.parse(new ByteArrayInputStream(server.getTestXml().getBytes(UTF_8)));
        } catch (Exception e) {
            // not important if it fails
        }
        assertThat(server.getHits(), Matchers.greaterThan(0));
    }

        String xml = server.getTestXml();
        assertEquals(xml, format(xml, 1));
        assertEquals(0, server.getHits());

    @SuppressWarnings("deprecation")
    @Test
    public void testGetSAXParserFactory() throws Exception {
        SAXParserFactory saxParserFactory = XmlUtil.getSAXParserFactory();
        assertNotNull(saxParserFactory);
        // check if the XXE protection is enabled
        SAXParser saxParser = saxParserFactory.newSAXParser();
        assertThrows(SAXException.class,
                () -> saxParser.parse(new ByteArrayInputStream(server.getTestXml().getBytes(UTF_8)), new HandlerBase()));
        assertEquals(0, server.getHits());

        assertThrows(SAXException.class, () -> XmlUtil.setFeature(saxParserFactory, "test://no-such-feature"));
        ignoreXxeFailureProp.setOrClearProperty("false");
        assertThrows(SAXException.class, () -> XmlUtil.setFeature(saxParserFactory, "test://no-such-feature"));
        ignoreXxeFailureProp.setOrClearProperty("true");
        XmlUtil.setFeature(saxParserFactory, "test://no-such-feature");
    }

    @Test
    public void testGetXmlInputFactory() throws Exception {
        XMLInputFactory xmlInputFactory = XmlUtil.getXMLInputFactory();
        assertNotNull(xmlInputFactory);
        // check if the XXE protection is enabled
        assertThrows(XMLStreamException.class,
                () -> staxReadEvents(xmlInputFactory.createXMLEventReader(new StringReader(server.getTestXml()))));
        assertEquals(0, server.getHits());

        assertThrows(IllegalArgumentException.class,
                () -> XmlUtil.setProperty(xmlInputFactory, "test://no-such-property", false));
        ignoreXxeFailureProp.setOrClearProperty("false");
        assertThrows(IllegalArgumentException.class,
                () -> XmlUtil.setProperty(xmlInputFactory, "test://no-such-property", false));
        ignoreXxeFailureProp.setOrClearProperty("true");
        XmlUtil.setProperty(xmlInputFactory, "test://no-such-feature", false);
    }

    private void staxReadEvents(XMLEventReader reader) throws XMLStreamException {
        try {
            while (reader.hasNext()) {
                reader.nextEvent();
            }
        } finally {
            reader.close();
        }
    }

    static class DummyServer implements Runnable {
        private static final String XXE_TEST_STR_TEMPLATE = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
                + "  <!DOCTYPE test [\n" + "    <!ENTITY xxe SYSTEM \"%s\">\n" + "  ]>" + "<a><b>&xxe;</b></a>";

        private final ServerSocket serverSocket;
        private final AtomicInteger counter = new AtomicInteger();

        DummyServer() throws IOException {
            serverSocket = new ServerSocket(0, 5, InetAddress.getLoopbackAddress());
        }

        public void start() {
            new Thread(this, "DummyServer-acceptor").start();
        }

        public String getUrlString() {
            return "http://127.0.0.1:" + serverSocket.getLocalPort();
        }

        public String getTestXml() {
            return String.format(XXE_TEST_STR_TEMPLATE, getUrlString());
        }

        public int getHits() {
            return counter.get();
        }

        public void stop() {
            try {
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void run() {
            while (true) {
                try (Socket socket = serverSocket.accept()) {
                    System.out.println(">> connection accepted: " + socket);
                    counter.incrementAndGet();
                } catch (Exception e) {
                    System.out.println(">> stopping the server. Cause: " + e.getClass().getName());
                    return;
                }
            }
        }
    }