CVE-2020-13692: Improper Restriction of XML External Entity Reference (CWE-611)
value: PostgreSQL JDBC Driver (aka PgJDBC) before 42.2.13 allows XXE.
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: 7.7
Published: 2020-06-04T16:15Z
Repository: https://github.com/pgjdbc/pgjdbc
Language: Java
Files changed: pgjdbc/src/main/java/org/postgresql/PGProperty.java, pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java, pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java, pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java, pgjdbc/src/main/java/org/postgresql/jdbc/PgSQLXML.java, pgjdbc/src/main/java/org/postgresql/xml/DefaultPGXmlFactoryFactory.java, pgjdbc/src/main/java/org/postgresql/xml/EmptyStringEntityResolver.java, pgjdbc/src/main/java/org/postgresql/xml/LegacyInsecurePGXmlFactoryFactory.java, pgjdbc/src/main/java/org/postgresql/xml/NullErrorHandler.java, pgjdbc/src/main/java/org/postgresql/xml/PGXmlFactoryFactory.java, pgjdbc/src/test/java/org/postgresql/jdbc/PgSQLXMLTest.java
Commit date: 2020-06-01 08:54:32 -0400
Commit message: Merge pull request from GHSA-37xm-4h3m-5w3v

* refactor: Clean up whitespace in existing PgSQLXMLTest

* fix: Fix XXE vulnerability in PgSQLXML by disabling external access and doctypes

Fixes XXE vulnerability by defaulting to disabling external access and doc types. The
legacy insecure behavior can be restored via the new connection property xmlFactoryFactory
with a value of LEGACY_INSECURE. Alternatively, a custom class name can be specified that
implements org.postgresql.xml.PGXmlFactoryFactory and takes a no argument constructor.

* fix: Add missing getter and setter for XML_FACTORY_FACTORY to BasicDataSource
Diff: diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
index 49018891..f94b3878 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
@@ -659,10 +659,21 @@ public enum PGProperty {
   USE_SPNEGO(
     "useSpnego",
     "false",
     "Use SPNEGO in SSPI authentication requests"),
 
+  /**
+   * Factory class to instantiate factories for XML processing.
+   * The default factory disables external entity processing.
+   * Legacy behavior with external entity processing can be enabled by specifying a value of LEGACY_INSECURE.
+   * Or specify a custom class that implements {@code org.postgresql.xml.PGXmlFactoryFactory}.
+   */
+  XML_FACTORY_FACTORY(
+    "xmlFactoryFactory",
+    "",
+    "Factory class to instantiate factories for XML processing"),
+
   ;
 
   private final String name;
   private final String defaultValue;
   private final boolean required;
diff --git a/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java b/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java
index 2dbba964..56fb4b87 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java
@@ -8,10 +8,11 @@ package org.postgresql.core;
 import org.postgresql.PGConnection;
 import org.postgresql.PGProperty;
 import org.postgresql.jdbc.FieldMetadata;
 import org.postgresql.jdbc.TimestampUtils;
 import org.postgresql.util.LruCache;
+import org.postgresql.xml.PGXmlFactoryFactory;
 
 import java.sql.Connection;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.util.TimerTask;
@@ -210,6 +211,14 @@ public interface BaseConnection extends PGConnection, Connection {
    * @return Indication if hints to backend (such as when transaction begins)
    *         should be read only.
    * @see PGProperty#READ_ONLY_MODE
    */
   boolean hintReadOnly();
+
+  /**
+   * Retrieve the factory to instantiate XML processing factories.
+   *
+   * @return The factory to use to instantiate XML processing factories
+   * @throws SQLException if the class cannot be found or instantiated.
+   */
+  PGXmlFactoryFactory getXmlFactoryFactory() throws SQLException;
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
index bf596b9e..e4e2bb32 100644
--- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
@@ -1524,10 +1524,18 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
   //#endif
   public java.util.logging.Logger getParentLogger() {
     return Logger.getLogger("org.postgresql");
   }
 
+  public String getXmlFactoryFactory() {
+    return PGProperty.XML_FACTORY_FACTORY.get(properties);
+  }
+
+  public void setXmlFactoryFactory(String xmlFactoryFactory) {
+    PGProperty.XML_FACTORY_FACTORY.set(properties, xmlFactoryFactory);
+  }
+
   /*
    * Alias methods below, these are to help with ease-of-use with other database tools / frameworks
    * which expect normal java bean getters / setters to exist for the property names.
    */
 
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
index 17aa8985..e7afc931 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
@@ -35,10 +35,13 @@ import org.postgresql.util.HostSpec;
 import org.postgresql.util.LruCache;
 import org.postgresql.util.PGBinaryObject;
 import org.postgresql.util.PGobject;
 import org.postgresql.util.PSQLException;
 import org.postgresql.util.PSQLState;
+import org.postgresql.xml.DefaultPGXmlFactoryFactory;
+import org.postgresql.xml.LegacyInsecurePGXmlFactoryFactory;
+import org.postgresql.xml.PGXmlFactoryFactory;
 
 import java.io.IOException;
 import java.sql.Array;
 import java.sql.Blob;
 import java.sql.CallableStatement;
@@ -154,10 +157,13 @@ public class PgConnection implements BaseConnection {
    */
   private final boolean replicationConnection;
 
   private final LruCache<FieldMetadata.Key, FieldMetadata> fieldMetadataCache;
 
+  private final String xmlFactoryFactoryClass;
+  private PGXmlFactoryFactory xmlFactoryFactory;
+
   final CachedQuery borrowQuery(String sql) throws SQLException {
     return queryExecutor.borrowQuery(sql);
   }
 
   final CachedQuery borrowCallableQuery(String sql) throws SQLException {
@@ -309,10 +315,12 @@ public class PgConnection implements BaseConnection {
             Math.max(0, PGProperty.DATABASE_METADATA_CACHE_FIELDS.getInt(info)),
             Math.max(0, PGProperty.DATABASE_METADATA_CACHE_FIELDS_MIB.getInt(info) * 1024 * 1024),
         false);
 
     replicationConnection = PGProperty.REPLICATION.get(info) != null;
+
+    xmlFactoryFactoryClass = PGProperty.XML_FACTORY_FACTORY.get(info);
   }
 
   private static ReadOnlyBehavior getReadOnlyBehavior(String property) {
     try {
       return ReadOnlyBehavior.valueOf(property);
@@ -1821,6 +1829,38 @@ public class PgConnection implements BaseConnection {
   @Override
   public final String getParameterStatus(String parameterName) {
     return queryExecutor.getParameterStatus(parameterName);
   }
 
+  @Override
+  public PGXmlFactoryFactory getXmlFactoryFactory() throws SQLException {
+    if (xmlFactoryFactory == null) {
+      if (xmlFactoryFactoryClass == null || xmlFactoryFactoryClass.equals("")) {
+        xmlFactoryFactory = DefaultPGXmlFactoryFactory.INSTANCE;
+      } else if (xmlFactoryFactoryClass.equals("LEGACY_INSECURE")) {
+        xmlFactoryFactory = LegacyInsecurePGXmlFactoryFactory.INSTANCE;
+      } else {
+        Class<?> clazz;
+        try {
+          clazz = Class.forName(xmlFactoryFactoryClass);
+        } catch (ClassNotFoundException ex) {
+          throw new PSQLException(
+              GT.tr("Could not instantiate xmlFactoryFactory: {0}", xmlFactoryFactoryClass),
+              PSQLState.INVALID_PARAMETER_VALUE, ex);
+        }
+        if (!clazz.isAssignableFrom(PGXmlFactoryFactory.class)) {
+          throw new PSQLException(
+              GT.tr("Connection property xmlFactoryFactory must implement PGXmlFactoryFactory: {0}", xmlFactoryFactoryClass),
+              PSQLState.INVALID_PARAMETER_VALUE);
+        }
+        try {
+          xmlFactoryFactory = (PGXmlFactoryFactory) clazz.newInstance();
+        } catch (Exception ex) {
+          throw new PSQLException(
+              GT.tr("Could not instantiate xmlFactoryFactory: {0}", xmlFactoryFactoryClass),
+              PSQLState.INVALID_PARAMETER_VALUE, ex);
+        }
+      }
+    }
+    return xmlFactoryFactory;
+  }
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgSQLXML.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgSQLXML.java
index 525ac146..f919d7fd 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgSQLXML.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgSQLXML.java
@@ -7,14 +7,15 @@ package org.postgresql.jdbc;
 
 import org.postgresql.core.BaseConnection;
 import org.postgresql.util.GT;
 import org.postgresql.util.PSQLException;
 import org.postgresql.util.PSQLState;
+import org.postgresql.xml.DefaultPGXmlFactoryFactory;
+import org.postgresql.xml.PGXmlFactoryFactory;
 
-import org.xml.sax.ErrorHandler;
 import org.xml.sax.InputSource;
-import org.xml.sax.SAXParseException;
+import org.xml.sax.XMLReader;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -25,11 +26,10 @@ import java.io.StringWriter;
 import java.io.Writer;
 import java.sql.SQLException;
 import java.sql.SQLXML;
 
 import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.stream.XMLInputFactory;
 import javax.xml.stream.XMLOutputFactory;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamReader;
 import javax.xml.stream.XMLStreamWriter;
@@ -75,10 +75,17 @@ public class PgSQLXML implements SQLXML {
     this.initialized = initialized;
     this.active = false;
     this.freed = false;
   }
 
+  private PGXmlFactoryFactory getXmlFactoryFactory() throws SQLException {
+    if (conn != null) {
+      return conn.getXmlFactoryFactory();
+    }
+    return DefaultPGXmlFactoryFactory.INSTANCE;
+  }
+
   @Override
   public synchronized void free() {
     freed = true;
     data = null;
   }
@@ -130,22 +137,21 @@ public class PgSQLXML implements SQLXML {
       return null;
     }
 
     try {
       if (sourceClass == null || DOMSource.class.equals(sourceClass)) {
-        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
-        DocumentBuilder builder = factory.newDocumentBuilder();
-        builder.setErrorHandler(new NonPrintingErrorHandler());
+        DocumentBuilder builder = getXmlFactoryFactory().newDocumentBuilder();
         InputSource input = new InputSource(new StringReader(data));
         return (T) new DOMSource(builder.parse(input));
       } else if (SAXSource.class.equals(sourceClass)) {
+        XMLReader reader = getXmlFactoryFactory().createXMLReader();
         InputSource is = new InputSource(new StringReader(data));
-        return (T) new SAXSource(is);
+        return (T) new SAXSource(reader, is);
       } else if (StreamSource.class.equals(sourceClass)) {
         return (T) new StreamSource(new StringReader(data));
       } else if (StAXSource.class.equals(sourceClass)) {
-        XMLInputFactory xif = XMLInputFactory.newInstance();
+        XMLInputFactory xif = getXmlFactoryFactory().newXMLInputFactory();
         XMLStreamReader xsr = xif.createXMLStreamReader(new StringReader(data));
         return (T) new StAXSource(xsr);
       }
     } catch (Exception e) {
       throw new PSQLException(GT.tr("Unable to decode xml data."), PSQLState.DATA_ERROR, e);
@@ -189,12 +195,11 @@ public class PgSQLXML implements SQLXML {
       domResult = new DOMResult();
       active = true;
       return (T) domResult;
     } else if (SAXResult.class.equals(resultClass)) {
       try {
-        SAXTransformerFactory transformerFactory =
-            (SAXTransformerFactory) SAXTransformerFactory.newInstance();
+        SAXTransformerFactory transformerFactory = getXmlFactoryFactory().newSAXTransformerFactory();
         TransformerHandler transformerHandler = transformerFactory.newTransformerHandler();
         stringWriter = new StringWriter();
         transformerHandler.setResult(new StreamResult(stringWriter));
         active = true;
         return (T) new SAXResult(transformerHandler);
@@ -207,11 +212,11 @@ public class PgSQLXML implements SQLXML {
       active = true;
       return (T) new StreamResult(stringWriter);
     } else if (StAXResult.class.equals(resultClass)) {
       stringWriter = new StringWriter();
       try {
-        XMLOutputFactory xof = XMLOutputFactory.newInstance();
+        XMLOutputFactory xof = getXmlFactoryFactory().newXMLOutputFactory();
         XMLStreamWriter xsw = xof.createXMLStreamWriter(stringWriter);
         active = true;
         return (T) new StAXResult(xsw);
       } catch (XMLStreamException xse) {
         throw new PSQLException(GT.tr("Unable to create StAXResult for SQLXML"),
@@ -270,11 +275,11 @@ public class PgSQLXML implements SQLXML {
     } else if (domResult != null) {
       // Copy the content from the result to a source
       // and use the identify transform to get it into a
       // friendlier result format.
       try {
-        TransformerFactory factory = TransformerFactory.newInstance();
+        TransformerFactory factory = getXmlFactoryFactory().newTransformerFactory();
         Transformer transformer = factory.newTransformer();
         DOMSource domSource = new DOMSource(domResult.getNode());
         StringWriter stringWriter = new StringWriter();
         StreamResult streamResult = new StreamResult(stringWriter);
         transformer.transform(domSource, streamResult);
@@ -296,20 +301,6 @@ public class PgSQLXML implements SQLXML {
               "This SQLXML object has already been initialized, so you cannot manipulate it further."),
           PSQLState.OBJECT_NOT_IN_STATE);
     }
     initialized = true;
   }
-
-  // Don't clutter System.err with errors the user can't silence.
-  // If something bad really happens an exception will be thrown.
-  static class NonPrintingErrorHandler implements ErrorHandler {
-    public void error(SAXParseException e) {
-    }
-
-    public void fatalError(SAXParseException e) {
-    }
-
-    public void warning(SAXParseException e) {
-    }
-  }
-
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/xml/DefaultPGXmlFactoryFactory.java b/pgjdbc/src/main/java/org/postgresql/xml/DefaultPGXmlFactoryFactory.java
new file mode 100644
index 00000000..50b765da
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/xml/DefaultPGXmlFactoryFactory.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.xml;
+
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.sax.SAXTransformerFactory;
+
+/**
+ * Default implementation of PGXmlFactoryFactory that configures each factory per OWASP recommendations.
+ *
+ * @see <a href="https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html">https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html</a>
+ */
+public class DefaultPGXmlFactoryFactory implements PGXmlFactoryFactory {
+  public static final DefaultPGXmlFactoryFactory INSTANCE = new DefaultPGXmlFactoryFactory();
+
+  private DefaultPGXmlFactoryFactory() {
+  }
+
+  private DocumentBuilderFactory getDocumentBuilderFactory() {
+    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+    setFactoryProperties(factory);
+    factory.setXIncludeAware(false);
+    factory.setExpandEntityReferences(false);
+    return factory;
+  }
+
+  @Override
+  public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
+    DocumentBuilder builder = getDocumentBuilderFactory().newDocumentBuilder();
+    builder.setEntityResolver(EmptyStringEntityResolver.INSTANCE);
+    builder.setErrorHandler(NullErrorHandler.INSTANCE);
+    return builder;
+  }
+
+  @Override
+  public TransformerFactory newTransformerFactory() {
+    TransformerFactory factory = TransformerFactory.newInstance();
+    setFactoryProperties(factory);
+    return factory;
+  }
+
+  @Override
+  public SAXTransformerFactory newSAXTransformerFactory() {
+    SAXTransformerFactory factory = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
+    setFactoryProperties(factory);
+    return factory;
+  }
+
+  @Override
+  public XMLInputFactory newXMLInputFactory() {
+    XMLInputFactory factory = XMLInputFactory.newInstance();
+    setPropertyQuietly(factory, XMLInputFactory.SUPPORT_DTD, false);
+    setPropertyQuietly(factory, XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
+    return factory;
+  }
+
+  @Override
+  public XMLOutputFactory newXMLOutputFactory() {
+    XMLOutputFactory factory = XMLOutputFactory.newInstance();
+    return factory;
+  }
+
+  @Override
+  public XMLReader createXMLReader() throws SAXException {
+    XMLReader factory = XMLReaderFactory.createXMLReader();
+    setFeatureQuietly(factory, "http://apache.org/xml/features/disallow-doctype-decl", true);
+    setFeatureQuietly(factory, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+    setFeatureQuietly(factory, "http://xml.org/sax/features/external-general-entities", false);
+    setFeatureQuietly(factory, "http://xml.org/sax/features/external-parameter-entities", false);
+    factory.setErrorHandler(NullErrorHandler.INSTANCE);
+    return factory;
+  }
+
+  private static void setFeatureQuietly(Object factory, String name, boolean value) {
+    try {
+      if (factory instanceof DocumentBuilderFactory) {
+        ((DocumentBuilderFactory) factory).setFeature(name, value);
+      } else if (factory instanceof TransformerFactory) {
+        ((TransformerFactory) factory).setFeature(name, value);
+      } else if (factory instanceof XMLReader) {
+        ((XMLReader) factory).setFeature(name, value);
+      } else {
+        throw new Error("Invalid factory class: " + factory.getClass());
+      }
+      return;
+    } catch (Exception ignore) {
+    }
+  }
+
+  private static void setAttributeQuietly(Object factory, String name, Object value) {
+    try {
+      if (factory instanceof DocumentBuilderFactory) {
+        ((DocumentBuilderFactory) factory).setAttribute(name, value);
+      } else if (factory instanceof TransformerFactory) {
+        ((TransformerFactory) factory).setAttribute(name, value);
+      } else {
+        throw new Error("Invalid factory class: " + factory.getClass());
+      }
+    } catch (Exception ignore) {
+    }
+  }
+
+  private static void setFactoryProperties(Object factory) {
+    setFeatureQuietly(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
+    setFeatureQuietly(factory, "http://apache.org/xml/features/disallow-doctype-decl", true);
+    setFeatureQuietly(factory, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+    setFeatureQuietly(factory, "http://xml.org/sax/features/external-general-entities", false);
+    setFeatureQuietly(factory, "http://xml.org/sax/features/external-parameter-entities", false);
+    // Values from XMLConstants inlined for JDK 1.6 compatibility
+    setAttributeQuietly(factory, "http://javax.xml.XMLConstants/property/accessExternalDTD", "");
+    setAttributeQuietly(factory, "http://javax.xml.XMLConstants/property/accessExternalSchema", "");
+    setAttributeQuietly(factory, "http://javax.xml.XMLConstants/property/accessExternalStylesheet", "");
+  }
+
+  private static void setPropertyQuietly(Object factory, String name, Object value) {
+    try {
+      if (factory instanceof XMLReader) {
+        ((XMLReader) factory).setProperty(name, value);
+      } else if (factory instanceof XMLInputFactory) {
+        ((XMLInputFactory) factory).setProperty(name, value);
+      } else {
+        throw new Error("Invalid factory class: " + factory.getClass());
+      }
+    } catch (Exception ignore) {
+    }
+  }
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/xml/EmptyStringEntityResolver.java b/pgjdbc/src/main/java/org/postgresql/xml/EmptyStringEntityResolver.java
new file mode 100644
index 00000000..e96a39b8
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/xml/EmptyStringEntityResolver.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.xml;
+
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+public class EmptyStringEntityResolver implements EntityResolver {
+  public static final EmptyStringEntityResolver INSTANCE = new EmptyStringEntityResolver();
+
+  @Override
+  public InputSource resolveEntity(String publicId, String systemId)
+      throws SAXException, IOException {
+    return new InputSource(new StringReader(""));
+  }
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/xml/LegacyInsecurePGXmlFactoryFactory.java b/pgjdbc/src/main/java/org/postgresql/xml/LegacyInsecurePGXmlFactoryFactory.java
new file mode 100644
index 00000000..ac90fa4c
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/xml/LegacyInsecurePGXmlFactoryFactory.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.xml;
+
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.sax.SAXTransformerFactory;
+
+public class LegacyInsecurePGXmlFactoryFactory implements PGXmlFactoryFactory {
+  public static final LegacyInsecurePGXmlFactoryFactory INSTANCE = new LegacyInsecurePGXmlFactoryFactory();
+
+  private LegacyInsecurePGXmlFactoryFactory() {
+  }
+
+  @Override
+  public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
+    DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+    builder.setErrorHandler(NullErrorHandler.INSTANCE);
+    return builder;
+  }
+
+  @Override
+  public TransformerFactory newTransformerFactory() {
+    return TransformerFactory.newInstance();
+  }
+
+  @Override
+  public SAXTransformerFactory newSAXTransformerFactory() {
+    return (SAXTransformerFactory) SAXTransformerFactory.newInstance();
+  }
+
+  @Override
+  public XMLInputFactory newXMLInputFactory() {
+    return XMLInputFactory.newInstance();
+  }
+
+  @Override
+  public XMLOutputFactory newXMLOutputFactory() {
+    return XMLOutputFactory.newInstance();
+  }
+
+  @Override
+  public XMLReader createXMLReader() throws SAXException {
+    return XMLReaderFactory.createXMLReader();
+  }
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/xml/NullErrorHandler.java b/pgjdbc/src/main/java/org/postgresql/xml/NullErrorHandler.java
new file mode 100644
index 00000000..d12a4fa6
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/xml/NullErrorHandler.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.xml;
+
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.SAXParseException;
+
+/**
+ * Error handler that silently suppresses all errors.
+ */
+public class NullErrorHandler implements ErrorHandler {
+  public static final NullErrorHandler INSTANCE = new NullErrorHandler();
+
+  public void error(SAXParseException e) {
+  }
+
+  public void fatalError(SAXParseException e) {
+  }
+
+  public void warning(SAXParseException e) {
+  }
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/xml/PGXmlFactoryFactory.java b/pgjdbc/src/main/java/org/postgresql/xml/PGXmlFactoryFactory.java
new file mode 100644
index 00000000..d5c74d52
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/xml/PGXmlFactoryFactory.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2020, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.xml;
+
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.sax.SAXTransformerFactory;
+
+public interface PGXmlFactoryFactory {
+  DocumentBuilder newDocumentBuilder() throws ParserConfigurationException;
+
+  TransformerFactory newTransformerFactory();
+
+  SAXTransformerFactory newSAXTransformerFactory();
+
+  XMLInputFactory newXMLInputFactory();
+
+  XMLOutputFactory newXMLOutputFactory();
+
+  XMLReader createXMLReader() throws SAXException;
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/PgSQLXMLTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/PgSQLXMLTest.java
index 53fd56dc..461ad037 100644
--- a/pgjdbc/src/test/java/org/postgresql/jdbc/PgSQLXMLTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/jdbc/PgSQLXMLTest.java
@@ -5,46 +5,136 @@
 
 package org.postgresql.jdbc;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
+import org.postgresql.PGProperty;
+import org.postgresql.core.BaseConnection;
 import org.postgresql.test.TestUtil;
 import org.postgresql.test.jdbc2.BaseTest4;
 
 import org.junit.Before;
 import org.junit.Test;
 
+import java.io.StringWriter;
 import java.io.Writer;
+import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
+import java.sql.SQLException;
 import java.sql.SQLXML;
 import java.sql.Statement;
+import java.util.Properties;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+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.sax.SAXSource;
+import javax.xml.transform.stax.StAXSource;
+import javax.xml.transform.stream.StreamResult;
 
 public class PgSQLXMLTest extends BaseTest4 {
 
   @Override
   @Before
   public void setUp() throws Exception {
     super.setUp();
-    TestUtil.createTempTable(con, "xmltab","x xml");
+    TestUtil.createTempTable(con, "xmltab", "x xml");
   }
 
   @Test
-  public void setCharacterStream() throws  Exception {
+  public void setCharacterStream() throws Exception {
     String exmplar = "<x>value</x>";
     SQLXML pgSQLXML = con.createSQLXML();
     Writer writer = pgSQLXML.setCharacterStream();
     writer.write(exmplar);
     PreparedStatement preparedStatement = con.prepareStatement("insert into xmltab values (?)");
-    preparedStatement.setSQLXML(1,pgSQLXML);
+    preparedStatement.setSQLXML(1, pgSQLXML);
     preparedStatement.execute();
 
     Statement statement = con.createStatement();
     ResultSet rs = statement.executeQuery("select * from xmltab");
     assertTrue(rs.next());
     SQLXML result = rs.getSQLXML(1);
     assertNotNull(result);
     assertEquals(exmplar, result.getString());
   }
+
+  private static final String LICENSE_URL =
+      PgSQLXMLTest.class.getClassLoader().getResource("META-INF/LICENSE").toString();
+  private static final String XXE_EXAMPLE =
+      "<!DOCTYPE foo [<!ELEMENT foo ANY >\n"
+      + "<!ENTITY xxe SYSTEM \"" + LICENSE_URL + "\">]>"
+      + "<foo>&xxe;</foo>";
+
+  @Test
+  public void testLegacyXxe() throws Exception {
+    Properties props = new Properties();
+    props.setProperty(PGProperty.XML_FACTORY_FACTORY.getName(), "LEGACY_INSECURE");
+    try (Connection conn = TestUtil.openDB(props)) {
+      BaseConnection baseConn = conn.unwrap(BaseConnection.class);
+      PgSQLXML xml = new PgSQLXML(baseConn, XXE_EXAMPLE);
+      xml.getSource(null);
+    }
+  }
+
+  private static String sourceToString(Source source) throws TransformerException {
+    StringWriter sw = new StringWriter();
+    Transformer transformer = TransformerFactory.newInstance().newTransformer();
+    transformer.transform(source, new StreamResult(sw));
+    return sw.toString();
+  }
+
+  private <T extends Source> void testGetSourceXxe(Class<T> clazz) {
+    SQLException ex = assertThrows(SQLException.class, () -> {
+      PgSQLXML xml = new PgSQLXML(null, XXE_EXAMPLE);
+      xml.getSource(clazz);
+    });
+    String message = ex.getCause().getMessage();
+    assertTrue(
+        "Expected to get a <<DOCTYPE disallowed>> SAXParseException. Actual message is " + message,
+        message.startsWith("DOCTYPE is disallowed"));
+  }
+
+  @Test
+  public void testGetSourceXxeNull() throws Exception {
+    testGetSourceXxe(null);
+  }
+
+  @Test
+  public void testGetSourceXxeDOMSource() throws Exception {
+    testGetSourceXxe(DOMSource.class);
+  }
+
+  @Test
+  public void testGetSourceXxeSAXSource() throws Exception {
+    PgSQLXML xml = new PgSQLXML(null, XXE_EXAMPLE);
+    SAXSource source = xml.getSource(SAXSource.class);
+    TransformerException ex = assertThrows(TransformerException.class, () -> {
+      sourceToString(source);
+    });
+    String message = ex.getCause().getMessage();
+    assertTrue(
+        "Expected to get a <<DOCTYPE disallowed>> TransformerException. Actual message is " + message,
+        message.startsWith("DOCTYPE is disallowed"));
+  }
+
+  @Test
+  public void testGetSourceXxeStAXSource() throws Exception {
+    PgSQLXML xml = new PgSQLXML(null, XXE_EXAMPLE);
+    StAXSource source = xml.getSource(StAXSource.class);
+    XMLStreamReader reader = source.getXMLStreamReader();
+    // STAX will not throw XXE error until we actually read the element
+    assertThrows(XMLStreamException.class, () -> {
+      while (reader.hasNext()) {
+        reader.next();
+      }
+    });
+  }
 }

Vulnerable code: import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;
import javax.xml.parsers.DocumentBuilderFactory;
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        builder.setErrorHandler(new NonPrintingErrorHandler());
        return (T) new SAXSource(is);
        XMLInputFactory xif = XMLInputFactory.newInstance();
        SAXTransformerFactory transformerFactory =
            (SAXTransformerFactory) SAXTransformerFactory.newInstance();
        XMLOutputFactory xof = XMLOutputFactory.newInstance();
        TransformerFactory factory = TransformerFactory.newInstance();

  // Don't clutter System.err with errors the user can't silence.
  // If something bad really happens an exception will be thrown.
  static class NonPrintingErrorHandler implements ErrorHandler {
    public void error(SAXParseException e) {
    }

    public void fatalError(SAXParseException e) {
    }

    public void warning(SAXParseException e) {
    }
  }

    TestUtil.createTempTable(con, "xmltab","x xml");
  public void setCharacterStream() throws  Exception {
    preparedStatement.setSQLXML(1,pgSQLXML);
Fixed code:   /**
   * Factory class to instantiate factories for XML processing.
   * The default factory disables external entity processing.
   * Legacy behavior with external entity processing can be enabled by specifying a value of LEGACY_INSECURE.
   * Or specify a custom class that implements {@code org.postgresql.xml.PGXmlFactoryFactory}.
   */
  XML_FACTORY_FACTORY(
    "xmlFactoryFactory",
    "",
    "Factory class to instantiate factories for XML processing"),

import org.postgresql.xml.PGXmlFactoryFactory;

  /**
   * Retrieve the factory to instantiate XML processing factories.
   *
   * @return The factory to use to instantiate XML processing factories
   * @throws SQLException if the class cannot be found or instantiated.
   */
  PGXmlFactoryFactory getXmlFactoryFactory() throws SQLException;
  public String getXmlFactoryFactory() {
    return PGProperty.XML_FACTORY_FACTORY.get(properties);
  }

  public void setXmlFactoryFactory(String xmlFactoryFactory) {
    PGProperty.XML_FACTORY_FACTORY.set(properties, xmlFactoryFactory);
  }

import org.postgresql.xml.DefaultPGXmlFactoryFactory;
import org.postgresql.xml.LegacyInsecurePGXmlFactoryFactory;
import org.postgresql.xml.PGXmlFactoryFactory;
  private final String xmlFactoryFactoryClass;
  private PGXmlFactoryFactory xmlFactoryFactory;


    xmlFactoryFactoryClass = PGProperty.XML_FACTORY_FACTORY.get(info);
  @Override
  public PGXmlFactoryFactory getXmlFactoryFactory() throws SQLException {
    if (xmlFactoryFactory == null) {
      if (xmlFactoryFactoryClass == null || xmlFactoryFactoryClass.equals("")) {
        xmlFactoryFactory = DefaultPGXmlFactoryFactory.INSTANCE;
      } else if (xmlFactoryFactoryClass.equals("LEGACY_INSECURE")) {
        xmlFactoryFactory = LegacyInsecurePGXmlFactoryFactory.INSTANCE;
      } else {
        Class<?> clazz;
        try {
          clazz = Class.forName(xmlFactoryFactoryClass);
        } catch (ClassNotFoundException ex) {
          throw new PSQLException(
              GT.tr("Could not instantiate xmlFactoryFactory: {0}", xmlFactoryFactoryClass),
              PSQLState.INVALID_PARAMETER_VALUE, ex);
        }
        if (!clazz.isAssignableFrom(PGXmlFactoryFactory.class)) {
          throw new PSQLException(
              GT.tr("Connection property xmlFactoryFactory must implement PGXmlFactoryFactory: {0}", xmlFactoryFactoryClass),
              PSQLState.INVALID_PARAMETER_VALUE);
        }
        try {
          xmlFactoryFactory = (PGXmlFactoryFactory) clazz.newInstance();
        } catch (Exception ex) {
          throw new PSQLException(
              GT.tr("Could not instantiate xmlFactoryFactory: {0}", xmlFactoryFactoryClass),
              PSQLState.INVALID_PARAMETER_VALUE, ex);
        }
      }
    }
    return xmlFactoryFactory;
  }
import org.postgresql.xml.DefaultPGXmlFactoryFactory;
import org.postgresql.xml.PGXmlFactoryFactory;
import org.xml.sax.XMLReader;
  private PGXmlFactoryFactory getXmlFactoryFactory() throws SQLException {
    if (conn != null) {
      return conn.getXmlFactoryFactory();
    }
    return DefaultPGXmlFactoryFactory.INSTANCE;
  }

        DocumentBuilder builder = getXmlFactoryFactory().newDocumentBuilder();
        XMLReader reader = getXmlFactoryFactory().createXMLReader();
        return (T) new SAXSource(reader, is);
        XMLInputFactory xif = getXmlFactoryFactory().newXMLInputFactory();
        SAXTransformerFactory transformerFactory = getXmlFactoryFactory().newSAXTransformerFactory();
        XMLOutputFactory xof = getXmlFactoryFactory().newXMLOutputFactory();
        TransformerFactory factory = getXmlFactoryFactory().newTransformerFactory();
/*
 * Copyright (c) 2020, PostgreSQL Global Development Group
 * See the LICENSE file in the project root for more information.
 */

package org.postgresql.xml;

import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;

/**
 * Default implementation of PGXmlFactoryFactory that configures each factory per OWASP recommendations.
 *
 * @see <a href="https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html">https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html</a>
 */
public class DefaultPGXmlFactoryFactory implements PGXmlFactoryFactory {
  public static final DefaultPGXmlFactoryFactory INSTANCE = new DefaultPGXmlFactoryFactory();

  private DefaultPGXmlFactoryFactory() {
  }

  private DocumentBuilderFactory getDocumentBuilderFactory() {
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    setFactoryProperties(factory);
    factory.setXIncludeAware(false);
    factory.setExpandEntityReferences(false);
    return factory;
  }

  @Override
  public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
    DocumentBuilder builder = getDocumentBuilderFactory().newDocumentBuilder();
    builder.setEntityResolver(EmptyStringEntityResolver.INSTANCE);
    builder.setErrorHandler(NullErrorHandler.INSTANCE);
    return builder;
  }

  @Override
  public TransformerFactory newTransformerFactory() {
    TransformerFactory factory = TransformerFactory.newInstance();
    setFactoryProperties(factory);
    return factory;
  }

  @Override
  public SAXTransformerFactory newSAXTransformerFactory() {
    SAXTransformerFactory factory = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
    setFactoryProperties(factory);
    return factory;
  }

  @Override
  public XMLInputFactory newXMLInputFactory() {
    XMLInputFactory factory = XMLInputFactory.newInstance();
    setPropertyQuietly(factory, XMLInputFactory.SUPPORT_DTD, false);
    setPropertyQuietly(factory, XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
    return factory;
  }

  @Override
  public XMLOutputFactory newXMLOutputFactory() {
    XMLOutputFactory factory = XMLOutputFactory.newInstance();
    return factory;
  }

  @Override
  public XMLReader createXMLReader() throws SAXException {
    XMLReader factory = XMLReaderFactory.createXMLReader();
    setFeatureQuietly(factory, "http://apache.org/xml/features/disallow-doctype-decl", true);
    setFeatureQuietly(factory, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
    setFeatureQuietly(factory, "http://xml.org/sax/features/external-general-entities", false);
    setFeatureQuietly(factory, "http://xml.org/sax/features/external-parameter-entities", false);
    factory.setErrorHandler(NullErrorHandler.INSTANCE);
    return factory;
  }

  private static void setFeatureQuietly(Object factory, String name, boolean value) {
    try {
      if (factory instanceof DocumentBuilderFactory) {
        ((DocumentBuilderFactory) factory).setFeature(name, value);
      } else if (factory instanceof TransformerFactory) {
        ((TransformerFactory) factory).setFeature(name, value);
      } else if (factory instanceof XMLReader) {
        ((XMLReader) factory).setFeature(name, value);
      } else {
        throw new Error("Invalid factory class: " + factory.getClass());
      }
      return;
    } catch (Exception ignore) {
    }
  }

  private static void setAttributeQuietly(Object factory, String name, Object value) {
    try {
      if (factory instanceof DocumentBuilderFactory) {
        ((DocumentBuilderFactory) factory).setAttribute(name, value);
      } else if (factory instanceof TransformerFactory) {
        ((TransformerFactory) factory).setAttribute(name, value);
      } else {
        throw new Error("Invalid factory class: " + factory.getClass());
      }
    } catch (Exception ignore) {
    }
  }

  private static void setFactoryProperties(Object factory) {
    setFeatureQuietly(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
    setFeatureQuietly(factory, "http://apache.org/xml/features/disallow-doctype-decl", true);
    setFeatureQuietly(factory, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
    setFeatureQuietly(factory, "http://xml.org/sax/features/external-general-entities", false);
    setFeatureQuietly(factory, "http://xml.org/sax/features/external-parameter-entities", false);
    // Values from XMLConstants inlined for JDK 1.6 compatibility
    setAttributeQuietly(factory, "http://javax.xml.XMLConstants/property/accessExternalDTD", "");
    setAttributeQuietly(factory, "http://javax.xml.XMLConstants/property/accessExternalSchema", "");
    setAttributeQuietly(factory, "http://javax.xml.XMLConstants/property/accessExternalStylesheet", "");
  }

  private static void setPropertyQuietly(Object factory, String name, Object value) {
    try {
      if (factory instanceof XMLReader) {
        ((XMLReader) factory).setProperty(name, value);
      } else if (factory instanceof XMLInputFactory) {
        ((XMLInputFactory) factory).setProperty(name, value);
      } else {
        throw new Error("Invalid factory class: " + factory.getClass());
      }
    } catch (Exception ignore) {
    }
  }
}
/*
 * Copyright (c) 2020, PostgreSQL Global Development Group
 * See the LICENSE file in the project root for more information.
 */

package org.postgresql.xml;

import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.io.StringReader;

public class EmptyStringEntityResolver implements EntityResolver {
  public static final EmptyStringEntityResolver INSTANCE = new EmptyStringEntityResolver();

  @Override
  public InputSource resolveEntity(String publicId, String systemId)
      throws SAXException, IOException {
    return new InputSource(new StringReader(""));
  }
}
/*
 * Copyright (c) 2020, PostgreSQL Global Development Group
 * See the LICENSE file in the project root for more information.
 */

package org.postgresql.xml;

import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;

public class LegacyInsecurePGXmlFactoryFactory implements PGXmlFactoryFactory {
  public static final LegacyInsecurePGXmlFactoryFactory INSTANCE = new LegacyInsecurePGXmlFactoryFactory();

  private LegacyInsecurePGXmlFactoryFactory() {
  }

  @Override
  public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
    DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
    builder.setErrorHandler(NullErrorHandler.INSTANCE);
    return builder;
  }

  @Override
  public TransformerFactory newTransformerFactory() {
    return TransformerFactory.newInstance();
  }

  @Override
  public SAXTransformerFactory newSAXTransformerFactory() {
    return (SAXTransformerFactory) SAXTransformerFactory.newInstance();
  }

  @Override
  public XMLInputFactory newXMLInputFactory() {
    return XMLInputFactory.newInstance();
  }

  @Override
  public XMLOutputFactory newXMLOutputFactory() {
    return XMLOutputFactory.newInstance();
  }

  @Override
  public XMLReader createXMLReader() throws SAXException {
    return XMLReaderFactory.createXMLReader();
  }
}
/*
 * Copyright (c) 2020, PostgreSQL Global Development Group
 * See the LICENSE file in the project root for more information.
 */

package org.postgresql.xml;

import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;

/**
 * Error handler that silently suppresses all errors.
 */
public class NullErrorHandler implements ErrorHandler {
  public static final NullErrorHandler INSTANCE = new NullErrorHandler();

  public void error(SAXParseException e) {
  }

  public void fatalError(SAXParseException e) {
  }

  public void warning(SAXParseException e) {
  }
}
/*
 * Copyright (c) 2020, PostgreSQL Global Development Group
 * See the LICENSE file in the project root for more information.
 */

package org.postgresql.xml;

import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;

public interface PGXmlFactoryFactory {
  DocumentBuilder newDocumentBuilder() throws ParserConfigurationException;

  TransformerFactory newTransformerFactory();

  SAXTransformerFactory newSAXTransformerFactory();

  XMLInputFactory newXMLInputFactory();

  XMLOutputFactory newXMLOutputFactory();

  XMLReader createXMLReader() throws SAXException;
}
import static org.junit.Assert.assertThrows;
import org.postgresql.PGProperty;
import org.postgresql.core.BaseConnection;
import java.io.StringWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
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.sax.SAXSource;
import javax.xml.transform.stax.StAXSource;
import javax.xml.transform.stream.StreamResult;
    TestUtil.createTempTable(con, "xmltab", "x xml");
  public void setCharacterStream() throws Exception {
    preparedStatement.setSQLXML(1, pgSQLXML);

  private static final String LICENSE_URL =
      PgSQLXMLTest.class.getClassLoader().getResource("META-INF/LICENSE").toString();
  private static final String XXE_EXAMPLE =
      "<!DOCTYPE foo [<!ELEMENT foo ANY >\n"
      + "<!ENTITY xxe SYSTEM \"" + LICENSE_URL + "\">]>"
      + "<foo>&xxe;</foo>";

  @Test
  public void testLegacyXxe() throws Exception {
    Properties props = new Properties();
    props.setProperty(PGProperty.XML_FACTORY_FACTORY.getName(), "LEGACY_INSECURE");
    try (Connection conn = TestUtil.openDB(props)) {
      BaseConnection baseConn = conn.unwrap(BaseConnection.class);
      PgSQLXML xml = new PgSQLXML(baseConn, XXE_EXAMPLE);
      xml.getSource(null);
    }
  }

  private static String sourceToString(Source source) throws TransformerException {
    StringWriter sw = new StringWriter();
    Transformer transformer = TransformerFactory.newInstance().newTransformer();
    transformer.transform(source, new StreamResult(sw));
    return sw.toString();
  }

  private <T extends Source> void testGetSourceXxe(Class<T> clazz) {
    SQLException ex = assertThrows(SQLException.class, () -> {
      PgSQLXML xml = new PgSQLXML(null, XXE_EXAMPLE);
      xml.getSource(clazz);
    });
    String message = ex.getCause().getMessage();
    assertTrue(
        "Expected to get a <<DOCTYPE disallowed>> SAXParseException. Actual message is " + message,
        message.startsWith("DOCTYPE is disallowed"));
  }

  @Test
  public void testGetSourceXxeNull() throws Exception {
    testGetSourceXxe(null);
  }

  @Test
  public void testGetSourceXxeDOMSource() throws Exception {
    testGetSourceXxe(DOMSource.class);
  }

  @Test
  public void testGetSourceXxeSAXSource() throws Exception {
    PgSQLXML xml = new PgSQLXML(null, XXE_EXAMPLE);
    SAXSource source = xml.getSource(SAXSource.class);
    TransformerException ex = assertThrows(TransformerException.class, () -> {
      sourceToString(source);
    });
    String message = ex.getCause().getMessage();
    assertTrue(
        "Expected to get a <<DOCTYPE disallowed>> TransformerException. Actual message is " + message,
        message.startsWith("DOCTYPE is disallowed"));
  }

  @Test
  public void testGetSourceXxeStAXSource() throws Exception {
    PgSQLXML xml = new PgSQLXML(null, XXE_EXAMPLE);
    StAXSource source = xml.getSource(StAXSource.class);
    XMLStreamReader reader = source.getXMLStreamReader();
    // STAX will not throw XXE error until we actually read the element
    assertThrows(XMLStreamException.class, () -> {
      while (reader.hasNext()) {
        reader.next();
      }
    });
  }