Blob Blame History Raw
From 3b2a73ad85da069637a73beca432950204535979 Mon Sep 17 00:00:00 2001
From: Ondrej Dubaj <odubaj@redhat.com>
Date: Wed, 22 Jul 2020 11:39:42 +0200
Subject: [PATCH] Fix for 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.

* refactor: Clean up whitespace in existing PgSQLXMLTest
* fix: Fix XXE vulnerability in PgSQLXML by disabling external access and doctypes
* fix: Add missing getter and setter for XML_FACTORY_FACTORY to BasicDataSource
---
 .../main/java/org/postgresql/PGProperty.java  |  11 ++
 .../org/postgresql/core/BaseConnection.java   |   9 ++
 .../postgresql/ds/common/BaseDataSource.java  |   8 +
 .../org/postgresql/jdbc/PgConnection.java     |  41 +++++
 .../java/org/postgresql/jdbc/PgSQLXML.java    |  44 +++---
 .../xml/DefaultPGXmlFactoryFactory.java       | 141 ++++++++++++++++++
 .../xml/EmptyStringEntityResolver.java        |  23 +++
 .../LegacyInsecurePGXmlFactoryFactory.java    |  57 +++++++
 .../org/postgresql/xml/NullErrorHandler.java  |  25 ++++
 .../postgresql/xml/PGXmlFactoryFactory.java   |  30 ++++
 .../org/postgresql/jdbc/PgSQLXMLTest.java     | 124 +++++++++++++++
 .../postgresql/test/jdbc2/Jdbc2TestSuite.java |   2 +
 12 files changed, 489 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
 create mode 100644 pgjdbc/src/test/java/org/postgresql/jdbc/PgSQLXMLTest.java

diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
index e56e05e..7c2eed8 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
@@ -331,6 +331,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>
diff --git a/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java b/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java
index 1d316a0..5f85964 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;
@@ -202,4 +203,12 @@ public interface BaseConnection extends PGConnection, Connection {
    * @param flushCacheOnDeallocate true if statement cache should be reset when "deallocate/discard" message observed
    */
   void setFlushCacheOnDeallocate(boolean flushCacheOnDeallocate);
+
+  /**
+   * 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 268d936..2fb4e06 100644
--- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
@@ -1313,4 +1313,12 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
     return Logger.getLogger("org.postgresql");
   }
   //#endif
+
+  public String getXmlFactoryFactory() {
+    return PGProperty.XML_FACTORY_FACTORY.get(properties);
+  }
+
+  public void setXmlFactoryFactory(String xmlFactoryFactory) {
+    PGProperty.XML_FACTORY_FACTORY.set(properties, xmlFactoryFactory);
+  }
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
index 7140ab4..c9c4ada 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..dd7d5ac 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);
       }
@@ -168,6 +174,7 @@ public class PgSQLXML implements SQLXML {
   public synchronized Writer setCharacterStream() throws SQLException {
     checkFreed();
     initialize();
+    _active = true;
     _stringWriter = new StringWriter();
     return _stringWriter;
   }
@@ -182,8 +189,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 +206,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 +268,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 +295,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..b6a381d
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/xml/DefaultPGXmlFactoryFactory.java
@@ -0,0 +1,141 @@
+
+/*
+ * 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) {
+    }
+  }
+}
\ No newline at end of file
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..39227e0
--- /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(""));
+  }
+}
\ No newline at end of file
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..ed7a66b
--- /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();
+  }
+}
\ No newline at end of file
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..ad486c7
--- /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) {
+  }
+}
\ No newline at end of file
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..4bb98e4
--- /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;
+}
\ No newline at end of file
diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/PgSQLXMLTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/PgSQLXMLTest.java
new file mode 100644
index 0000000..49e389c
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/jdbc/PgSQLXMLTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2019, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.jdbc;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+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");
+  }
+
+  @Test
+  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.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();
+  }
+
+  @Test(expected = SQLException.class)
+  public void testGetSourceXxeNull() throws Exception {
+    PgSQLXML xml = new PgSQLXML(null, XXE_EXAMPLE);
+      xml.getSource(null);
+  }
+
+  @Test(expected = SQLException.class)
+  public void testGetSourceXxeDOMSource() throws Exception {
+    PgSQLXML xml = new PgSQLXML(null, XXE_EXAMPLE);
+      xml.getSource(DOMSource.class);
+  }
+
+  @Test(expected = TransformerException.class)
+  public void testGetSourceXxeSAXSource() throws Exception {
+    PgSQLXML xml = new PgSQLXML(null, XXE_EXAMPLE);
+    SAXSource source = xml.getSource(SAXSource.class);
+    sourceToString(source);
+    
+  }
+
+  @Test(expected = XMLStreamException.class)
+  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
+    while (reader.hasNext()) {
+      reader.next();
+    }
+  }
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java
index 6314d21..814288c 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java
@@ -13,6 +13,7 @@ import org.postgresql.core.ParserTest;
 import org.postgresql.core.ReturningParserTest;
 import org.postgresql.core.v3.V3ParameterListTests;
 import org.postgresql.jdbc.DeepBatchedInsertStatementTest;
+import org.postgresql.jdbc.PgSQLXMLTest;
 import org.postgresql.jdbc.PrimitiveArraySupportTest;
 import org.postgresql.test.core.JavaVersionTest;
 import org.postgresql.test.core.NativeQueryBindLengthTest;
@@ -76,6 +77,7 @@ import org.junit.runners.Suite;
         TimestampTest.class,
         TimezoneTest.class,
         PGTimeTest.class,
+        PgSQLXMLTest.class,
         PGTimestampTest.class,
         TimezoneCachingTest.class,
         ParserTest.class,
-- 
2.24.1