CVE-2020-13692 (Java, published 2020-06-04T16:15Z)
Description: value: PostgreSQL JDBC Driver (aka PgJDBC) before 42.2.13 allows XXE.
Severity: MEDIUM (CVSS v3 base score: 7.7)
Weakness: CWE-611 \u2014 Improper Restriction of XML External Entity Reference
The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output.
Repository: https://github.com/pgjdbc/pgjdbc
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 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

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();
      }
    });
  }