CVE-2021-23463: Improper Restriction of XML External Entity Reference (CWE-611)
value: The package com.h2database:h2 from 1.4.198 and before 2.0.202 are vulnerable to XML External Entity (XXE) Injection via the org.h2.jdbc.JdbcSQLXML class object, when it receives parsed string data from org.h2.jdbc.JdbcResultSet.getSQLXML() method. If it executes the getSource() method when the parameter is DOMSource.class it will trigger the vulnerability.
CWE description: 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.
CVSS v3 base score: 9.1
Published: 2021-12-10T20:15Z
Repository: https://github.com/h2database/h2database
Language: Java
Files changed: h2/src/main/org/h2/jdbc/JdbcSQLXML.java
Commit date: 2021-10-30 13:19:57 -0400
Commit message: fix for #3195 CQLXML XXE vulnerability

Diff: diff --git a/h2/src/main/org/h2/jdbc/JdbcSQLXML.java b/h2/src/main/org/h2/jdbc/JdbcSQLXML.java
index c497937a3..b60022797 100644
--- a/h2/src/main/org/h2/jdbc/JdbcSQLXML.java
+++ b/h2/src/main/org/h2/jdbc/JdbcSQLXML.java
@@ -14,18 +14,23 @@
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.io.Writer;
 import java.sql.SQLException;
 import java.sql.SQLXML;
+import java.util.HashMap;
+import java.util.Map;
 
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.stream.XMLInputFactory;
 import javax.xml.stream.XMLOutputFactory;
 import javax.xml.transform.Result;
 import javax.xml.transform.Source;
 import javax.xml.transform.Transformer;
 import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.URIResolver;
 import javax.xml.transform.dom.DOMResult;
 import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.sax.SAXResult;
 import javax.xml.transform.sax.SAXSource;
 import javax.xml.transform.sax.SAXTransformerFactory;
@@ -37,17 +42,33 @@
 
 import org.h2.message.DbException;
 import org.h2.message.TraceObject;
 import org.h2.value.Value;
 import org.w3c.dom.Node;
+import org.xml.sax.EntityResolver;
 import org.xml.sax.InputSource;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLReaderFactory;
 
 /**
  * Represents a SQLXML value.
  */
 public final class JdbcSQLXML extends JdbcLob implements SQLXML {
 
+    private static final Map<String,Boolean> secureFeatureMap = new HashMap<>();
+    private static final EntityResolver NOOP_ENTITY_RESOLVER = (publicId, systemId) -> new InputSource(new StringReader(""));
+    private static final URIResolver NOOP_URI_RESOLVER = (href, base) -> new StreamSource(new StringReader(""));
+
+    static {
+        secureFeatureMap.put(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        secureFeatureMap.put("http://apache.org/xml/features/disallow-doctype-decl", true);
+        secureFeatureMap.put("http://xml.org/sax/features/external-general-entities", false);
+        secureFeatureMap.put("http://xml.org/sax/features/external-parameter-entities", false);
+        secureFeatureMap.put("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+    }
+
     private DOMResult domResult;
 
     /**
      * Underlying stream for SAXResult, StAXResult, and StreamResult.
      */
@@ -105,19 +126,45 @@ public <T extends Source> T getSource(Class<T> sourceClass) throws SQLException
             if (isDebugEnabled()) {
                 debugCode(
                         "getSource(" + (sourceClass != null ? sourceClass.getSimpleName() + ".class" : "null") + ')');
             }
             checkReadable();
+            // According to https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
             if (sourceClass == null || sourceClass == DOMSource.class) {
                 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
-                return (T) new DOMSource(dbf.newDocumentBuilder().parse(new InputSource(value.getInputStream())));
+                for (Map.Entry<String,Boolean> entry : secureFeatureMap.entrySet()) {
+                    try {
+                        dbf.setFeature(entry.getKey(), entry.getValue());
+                    } catch (Exception ignore) {/**/}
+                }
+                dbf.setXIncludeAware(false);
+                dbf.setExpandEntityReferences(false);
+                dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
+                DocumentBuilder db = dbf.newDocumentBuilder();
+                db.setEntityResolver(NOOP_ENTITY_RESOLVER);
+                return (T) new DOMSource(db.parse(new InputSource(value.getInputStream())));
             } else if (sourceClass == SAXSource.class) {
-                return (T) new SAXSource(new InputSource(value.getInputStream()));
+                XMLReader reader = XMLReaderFactory.createXMLReader();
+                for (Map.Entry<String,Boolean> entry : secureFeatureMap.entrySet()) {
+                    try {
+                        reader.setFeature(entry.getKey(), entry.getValue());
+                    } catch (Exception ignore) {/**/}
+                }
+                reader.setEntityResolver(NOOP_ENTITY_RESOLVER);
+                return (T) new SAXSource(reader, new InputSource(value.getInputStream()));
             } else if (sourceClass == StAXSource.class) {
                 XMLInputFactory xif = XMLInputFactory.newInstance();
+                xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
+                xif.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+                xif.setProperty("javax.xml.stream.isSupportingExternalEntities", false);
                 return (T) new StAXSource(xif.createXMLStreamReader(value.getInputStream()));
             } else if (sourceClass == StreamSource.class) {
+                TransformerFactory tf = TransformerFactory.newInstance();
+                tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+                tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
+                tf.setURIResolver(NOOP_URI_RESOLVER);
+                tf.newTransformer().transform(new StreamSource(value.getInputStream()), new SAXResult(new DefaultHandler()));
                 return (T) new StreamSource(value.getInputStream());
             }
             throw unsupported(sourceClass.getName());
         } catch (Exception e) {
             throw logAndConvert(e);
@@ -163,11 +210,11 @@ public Writer setCharacterStream() throws SQLException {
     @Override
     public <T extends Result> T setResult(Class<T> resultClass) throws SQLException {
         try {
             if (isDebugEnabled()) {
                 debugCode(
-                        "getSource(" + (resultClass != null ? resultClass.getSimpleName() + ".class" : "null") + ')');
+                        "setResult(" + (resultClass != null ? resultClass.getSimpleName() + ".class" : "null") + ')');
             }
             checkEditable();
             if (resultClass == null || resultClass == DOMResult.class) {
                 domResult = new DOMResult();
                 state = State.SET_CALLED;

Vulnerable code:                 return (T) new DOMSource(dbf.newDocumentBuilder().parse(new InputSource(value.getInputStream())));
                return (T) new SAXSource(new InputSource(value.getInputStream()));
                        "getSource(" + (resultClass != null ? resultClass.getSimpleName() + ".class" : "null") + ')');
Fixed code: import java.util.HashMap;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.URIResolver;
import org.xml.sax.EntityResolver;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
    private static final Map<String,Boolean> secureFeatureMap = new HashMap<>();
    private static final EntityResolver NOOP_ENTITY_RESOLVER = (publicId, systemId) -> new InputSource(new StringReader(""));
    private static final URIResolver NOOP_URI_RESOLVER = (href, base) -> new StreamSource(new StringReader(""));

    static {
        secureFeatureMap.put(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        secureFeatureMap.put("http://apache.org/xml/features/disallow-doctype-decl", true);
        secureFeatureMap.put("http://xml.org/sax/features/external-general-entities", false);
        secureFeatureMap.put("http://xml.org/sax/features/external-parameter-entities", false);
        secureFeatureMap.put("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
    }

            // According to https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
                for (Map.Entry<String,Boolean> entry : secureFeatureMap.entrySet()) {
                    try {
                        dbf.setFeature(entry.getKey(), entry.getValue());
                    } catch (Exception ignore) {/**/}
                }
                dbf.setXIncludeAware(false);
                dbf.setExpandEntityReferences(false);
                dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
                DocumentBuilder db = dbf.newDocumentBuilder();
                db.setEntityResolver(NOOP_ENTITY_RESOLVER);
                return (T) new DOMSource(db.parse(new InputSource(value.getInputStream())));
                XMLReader reader = XMLReaderFactory.createXMLReader();
                for (Map.Entry<String,Boolean> entry : secureFeatureMap.entrySet()) {
                    try {
                        reader.setFeature(entry.getKey(), entry.getValue());
                    } catch (Exception ignore) {/**/}
                }
                reader.setEntityResolver(NOOP_ENTITY_RESOLVER);
                return (T) new SAXSource(reader, new InputSource(value.getInputStream()));
                xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
                xif.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
                xif.setProperty("javax.xml.stream.isSupportingExternalEntities", false);
                TransformerFactory tf = TransformerFactory.newInstance();
                tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
                tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
                tf.setURIResolver(NOOP_URI_RESOLVER);
                tf.newTransformer().transform(new StreamSource(value.getInputStream()), new SAXResult(new DefaultHandler()));
                        "setResult(" + (resultClass != null ? resultClass.getSimpleName() + ".class" : "null") + ')');