From: Markus Koschany <apo@debian.org>
Date: Sun, 3 Jul 2022 17:06:12 +0200
Subject: CVE-2020-13692

Bug-Debian: https://bugs.debian.org/962828
Origin: https://github.com/pgjdbc/pgjdbc/commit/14b62aca4764d496813f55a43d050b017e01eb65
---
 .../src/main/java/org/postgresql/PGProperty.java   |  12 ++
 .../java/org/postgresql/core/BaseConnection.java   |   9 ++
 .../org/postgresql/ds/common/BaseDataSource.java   |   8 ++
 .../java/org/postgresql/jdbc/PgConnection.java     |  41 ++++++
 .../main/java/org/postgresql/jdbc/PgSQLXML.java    |  43 +++----
 .../postgresql/xml/DefaultPGXmlFactoryFactory.java | 140 +++++++++++++++++++++
 .../postgresql/xml/EmptyStringEntityResolver.java  |  23 ++++
 .../xml/LegacyInsecurePGXmlFactoryFactory.java     |  57 +++++++++
 .../java/org/postgresql/xml/NullErrorHandler.java  |  25 ++++
 .../org/postgresql/xml/PGXmlFactoryFactory.java    |  30 +++++
 10 files changed, 362 insertions(+), 26 deletions(-)
 create mode 100644 pgjdbc/src/main/java/org/postgresql/xml/DefaultPGXmlFactoryFactory.java
 create mode 100644 pgjdbc/src/main/java/org/postgresql/xml/EmptyStringEntityResolver.java
 create mode 100644 pgjdbc/src/main/java/org/postgresql/xml/LegacyInsecurePGXmlFactoryFactory.java
 create mode 100644 pgjdbc/src/main/java/org/postgresql/xml/NullErrorHandler.java
 create mode 100644 pgjdbc/src/main/java/org/postgresql/xml/PGXmlFactoryFactory.java

diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
index 4afefe2..7864f32 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
@@ -336,6 +336,17 @@ 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"),
+
   /**
    * Force one of
    * <ul>
@@ -632,4 +643,5 @@ public enum PGProperty {
     }
     return null;
   }
+
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java b/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java
index 1d316a0..1d17c03 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java
@@ -9,6 +9,7 @@ import org.postgresql.PGConnection;
 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;
@@ -194,6 +195,14 @@ public interface BaseConnection extends PGConnection, Connection {
       String... columnNames)
       throws SQLException;
 
+  /**
+   * 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;
+
   /**
    * By default, the connection resets statement cache in case deallocate all/discard all
    * message is observed.
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 268d936..dc8deee 100644
--- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
@@ -52,6 +52,14 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
   // Map for all other properties
   private Properties properties = new Properties();
 
+  public String getXmlFactoryFactory() {
+    return PGProperty.XML_FACTORY_FACTORY.get(properties);
+  }
+
+  public void setXmlFactoryFactory(String xmlFactoryFactory) {
+    PGProperty.XML_FACTORY_FACTORY.set(properties, xmlFactoryFactory);
+  }
+
   /*
    * Ensure the driver is loaded as JDBC Driver might be invisible to Java's ServiceLoader.
    * Usually, {@code Class.forName(...)} is not required as {@link DriverManager} detects JDBC drivers
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
index 2bd09a2..a41c6d6 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
@@ -37,6 +37,9 @@ 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;
@@ -142,6 +145,9 @@ public class PgConnection implements BaseConnection {
 
   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);
   }
@@ -290,6 +296,8 @@ public class PgConnection implements BaseConnection {
         false);
 
     replicationConnection = PGProperty.REPLICATION.get(info) != null;
+
+    xmlFactoryFactoryClass = PGProperty.XML_FACTORY_FACTORY.get(info);
   }
 
   private static Set<Integer> getBinaryOids(Properties info) throws PSQLException {
@@ -1729,4 +1737,37 @@ public class PgConnection implements BaseConnection {
     }
     return ps;
   }
+
+  @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 9fb0eed..d76d4aa 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgSQLXML.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgSQLXML.java
@@ -9,10 +9,11 @@ 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;
@@ -27,7 +28,6 @@ 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;
@@ -77,6 +77,13 @@ public class PgSQLXML implements SQLXML {
     _freed = false;
   }
 
+  private PGXmlFactoryFactory getXmlFactoryFactory() throws SQLException {
+    if (_conn != null) {
+      return _conn.getXmlFactoryFactory();
+    }
+    return DefaultPGXmlFactoryFactory.INSTANCE;
+  }
+
   public synchronized void free() {
     _freed = true;
     _data = null;
@@ -128,18 +135,17 @@ public class PgSQLXML implements SQLXML {
 
     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);
       }
@@ -182,8 +188,7 @@ public class PgSQLXML implements SQLXML {
       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));
@@ -200,7 +205,7 @@ public class PgSQLXML implements SQLXML {
     } 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);
@@ -262,7 +267,7 @@ public class PgSQLXML implements SQLXML {
       // 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();
@@ -289,19 +294,5 @@ public class PgSQLXML implements SQLXML {
     }
     _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 0000000..50b765d
--- /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 0000000..e96a39b
--- /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 0000000..ac90fa4
--- /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 0000000..d12a4fa
--- /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 0000000..d5c74d5
--- /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;
+}
