CVE-2016-15026: Improper Restriction of XML External Entity Reference (CWE-611)
value: A vulnerability was found in 3breadt dd-plist 1.17 and classified as problematic. Affected by this issue is some unknown functionality. The manipulation leads to xml external entity reference. An attack has to be approached locally. Upgrading to version 1.18 is able to address this issue. The patch is identified as 8c954e8d9f6f6863729e50105a8abf3f87fff74c. It is recommended to upgrade the affected component. VDB-221486 is the identifier assigned to this vulnerability.
CWE description: The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output.
CVSS v3 base score: 7.8
Published: 2023-02-20T11:15Z
Repository: https://github.com/3breadt/dd-plist
Language: Other
Files changed: build.xml, src/main/java/com/dd/plist/PropertyList-1.0.dtd, src/main/java/com/dd/plist/XMLPropertyListParser.java
Commit date: 2016-10-04 20:51:43 -0500
Commit message: 1) Take steps to guard against external XXE attacks (except, note that <!DOCTYPE> cannot be disabled in XML plists).
2) Resolve the actual PLIST DTD from inside the JAR file itself, and prevent resolution of other external XML resources.
3) Make XMLPlistParser.getDocBuilder public

Diff: diff --git a/build.xml b/build.xml
index 5b65410..cff5188 100644
--- a/build.xml
+++ b/build.xml
@@ -32,11 +32,12 @@
            fork="true" />
   </target>
 
   <target name="jar" depends="compile">
     <jar destfile="${dist}/${jar}">
-     <fileset dir="${build}/classes"/>
+      <fileset dir="${build}/classes"/>
+      <fileset dir="${source}" includes="**/*.dtd"/>
     </jar>
   </target>
 
   <target name="javadoc" depends="init">
     <delete dir="${doc}/javadoc" />
diff --git a/src/main/java/com/dd/plist/PropertyList-1.0.dtd b/src/main/java/com/dd/plist/PropertyList-1.0.dtd
new file mode 100644
index 0000000..6a117a8
--- /dev/null
+++ b/src/main/java/com/dd/plist/PropertyList-1.0.dtd
@@ -0,0 +1,19 @@
+<!ENTITY % plistObject "(array | data | date | dict | real | integer | string | true | false )" >
+<!ELEMENT plist %plistObject;>
+<!ATTLIST plist version CDATA "1.0" >
+
+<!-- Collections -->
+<!ELEMENT array (%plistObject;)*>
+<!ELEMENT dict (key, %plistObject;)*>
+<!ELEMENT key (#PCDATA)>
+
+<!--- Primitive types -->
+<!ELEMENT string (#PCDATA)>
+<!ELEMENT data (#PCDATA)> <!-- Contents interpreted as Base-64 encoded -->
+<!ELEMENT date (#PCDATA)> <!-- Contents should conform to a subset of ISO 8601 (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'.  Smaller units may be omitted with a loss of precision) -->
+
+<!-- Numerical primitives -->
+<!ELEMENT true EMPTY>  <!-- Boolean constant true -->
+<!ELEMENT false EMPTY> <!-- Boolean constant false -->
+<!ELEMENT real (#PCDATA)> <!-- Contents should represent a floating point number matching ("+" | "-")? d+ ("."d*)? ("E" ("+" | "-") d+)? where d is a digit 0-9.  -->
+<!ELEMENT integer (#PCDATA)> <!-- Contents should represent a (possibly signed) integer number in base 10 -->
diff --git a/src/main/java/com/dd/plist/XMLPropertyListParser.java b/src/main/java/com/dd/plist/XMLPropertyListParser.java
index ab8c284..cc3e55e 100644
--- a/src/main/java/com/dd/plist/XMLPropertyListParser.java
+++ b/src/main/java/com/dd/plist/XMLPropertyListParser.java
@@ -30,10 +30,11 @@ import org.xml.sax.SAXException;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
 import java.io.ByteArrayInputStream;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.List;
@@ -42,54 +43,43 @@ import java.util.List;
  * Parses XML property lists.
  *
  * @author Daniel Dreibrodt
  */
 public class XMLPropertyListParser {
-
-    private static DocumentBuilderFactory docBuilderFactory = null;
-
-    /**
-     * Instantiation is prohibited by outside classes.
-     */
-    protected XMLPropertyListParser() {
-        /** empty **/
-    }
-
-    /**
-     * Initialize the document builder factory so that it can be reused and does not need to
-     * be reinitialized for each parse action.
-     */
-    private static synchronized void initDocBuilderFactory() {
-        docBuilderFactory = DocumentBuilderFactory.newInstance();
-        docBuilderFactory.setIgnoringComments(true);
-        docBuilderFactory.setCoalescing(true);
+    private static final DocumentBuilderFactory FACTORY = DocumentBuilderFactory.newInstance();
+    static {
+        try {
+            FACTORY.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+            Float v1_7 = new Float("1.7");
+            Float javaVersion = new Float(System.getProperty("java.specification.version"));
+            if (javaVersion.compareTo(v1_7) >= 0) {
+                FACTORY.setFeature("http://xml.org/sax/features/external-general-entities", false);
+                FACTORY.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+            }
+            FACTORY.setXIncludeAware(false);
+            FACTORY.setExpandEntityReferences(false);
+            FACTORY.setNamespaceAware(false);
+            FACTORY.setIgnoringComments(true);
+            FACTORY.setCoalescing(true);
+            FACTORY.setValidating(false);
+        } catch (ParserConfigurationException e) {
+            throw new RuntimeException(e);
+        }
     }
 
     /**
      * Gets a DocumentBuilder to parse a XML property list.
      * As DocumentBuilders are not thread-safe a new DocBuilder is generated for each request.
      *
      * @return A new DocBuilder that can parse property lists w/o an internet connection.
      * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing a XML property list
      *                                                        could not be created. This should not occur.
      */
-    private static synchronized DocumentBuilder getDocBuilder() throws ParserConfigurationException {
-        if (docBuilderFactory == null)
-            initDocBuilderFactory();
-        DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
-        docBuilder.setEntityResolver(new EntityResolver() {
-            public InputSource resolveEntity(String publicId, String systemId) {
-                if ("-//Apple Computer//DTD PLIST 1.0//EN".equals(publicId) || // older publicId
-                        "-//Apple//DTD PLIST 1.0//EN".equals(publicId)) { // newer publicId
-                    // return a dummy, zero length DTD so we don't have to fetch
-                    // it from the network.
-                    return new InputSource(new ByteArrayInputStream(new byte[0]));
-                }
-                return null;
-            }
-        });
-        return docBuilder;
+    public static synchronized DocumentBuilder getDocBuilder() throws ParserConfigurationException {
+        DocumentBuilder builder = FACTORY.newDocumentBuilder();
+        builder.setEntityResolver(new PlistDTDResolver());
+        return builder;
     }
 
     /**
      * Parses a XML property list file.
      *
@@ -101,16 +91,14 @@ public class XMLPropertyListParser {
      * @throws java.io.IOException If any IO error occurs while reading the file.
      * @throws org.xml.sax.SAXException If any parse error occurs.
      * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format.
      * @throws java.text.ParseException If a date string could not be parsed.
      */
-    public static NSObject parse(File f) throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException {
-        DocumentBuilder docBuilder = getDocBuilder();
-
-        Document doc = docBuilder.parse(f);
+    public static NSObject parse(File f)
+                throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException {
 
-        return parse(doc);
+        return parse(getDocBuilder().parse(new FileInputStream(f)));
     }
 
     /**
      * Parses a XML property list from a byte array.
      *
@@ -121,13 +109,14 @@ public class XMLPropertyListParser {
      * @throws java.io.IOException If any IO error occurs while reading the file.
      * @throws org.xml.sax.SAXException If any parse error occurs.
      * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format.
      * @throws java.text.ParseException If a date string could not be parsed.
      */
-    public static NSObject parse(final byte[] bytes) throws ParserConfigurationException, ParseException, SAXException, PropertyListFormatException, IOException {
-        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
-        return parse(bis);
+    public static NSObject parse(final byte[] bytes)
+                throws ParserConfigurationException, ParseException, SAXException, PropertyListFormatException, IOException {
+
+        return parse(new ByteArrayInputStream(bytes));
     }
 
     /**
      * Parses a XML property list from an input stream.
      *
@@ -139,16 +128,14 @@ public class XMLPropertyListParser {
      * @throws java.io.IOException If any IO error occurs while reading the file.
      * @throws org.xml.sax.SAXException If any parse error occurs.
      * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format.
      * @throws java.text.ParseException If a date string could not be parsed.
      */
-    public static NSObject parse(InputStream is) throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException {
-        DocumentBuilder docBuilder = getDocBuilder();
-
-        Document doc = docBuilder.parse(is);
+    public static NSObject parse(InputStream is)
+                throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException {
 
-        return parse(doc);
+        return parse(getDocBuilder().parse(is));
     }
 
     /**
      * Parses a property list from an XML document.
      *
@@ -288,6 +275,26 @@ public class XMLPropertyListParser {
             } else {
                 return "";
             }
         }
     }
+
+    /**
+     * Resolves only the Apple PLIST DTD.
+     */
+    static class PlistDTDResolver implements EntityResolver {
+        private static final String PLIST_SYSTEMID_1 = "-//Apple Computer//DTD PLIST 1.0//EN";
+        private static final String PLIST_SYSTEMID_2 = "-//Apple//DTD PLIST 1.0//EN";
+
+        PlistDTDResolver() {
+        }
+
+        // Implement EntityResolver
+
+        public InputSource resolveEntity(String publicId, String systemId) {
+            if (PLIST_SYSTEMID_1.equals(publicId) || PLIST_SYSTEMID_2.equals(publicId)) {
+                return new InputSource(XMLPropertyListParser.class.getResourceAsStream("PropertyList-1.0.dtd"));
+            }
+            return null;
+        }
+    }
 }

Vulnerable code:      <fileset dir="${build}/classes"/>

    private static DocumentBuilderFactory docBuilderFactory = null;

    /**
     * Instantiation is prohibited by outside classes.
     */
    protected XMLPropertyListParser() {
        /** empty **/
    }

    /**
     * Initialize the document builder factory so that it can be reused and does not need to
     * be reinitialized for each parse action.
     */
    private static synchronized void initDocBuilderFactory() {
        docBuilderFactory = DocumentBuilderFactory.newInstance();
        docBuilderFactory.setIgnoringComments(true);
        docBuilderFactory.setCoalescing(true);
    private static synchronized DocumentBuilder getDocBuilder() throws ParserConfigurationException {
        if (docBuilderFactory == null)
            initDocBuilderFactory();
        DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
        docBuilder.setEntityResolver(new EntityResolver() {
            public InputSource resolveEntity(String publicId, String systemId) {
                if ("-//Apple Computer//DTD PLIST 1.0//EN".equals(publicId) || // older publicId
                        "-//Apple//DTD PLIST 1.0//EN".equals(publicId)) { // newer publicId
                    // return a dummy, zero length DTD so we don't have to fetch
                    // it from the network.
                    return new InputSource(new ByteArrayInputStream(new byte[0]));
                }
                return null;
            }
        });
        return docBuilder;
    public static NSObject parse(File f) throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException {
        DocumentBuilder docBuilder = getDocBuilder();

        Document doc = docBuilder.parse(f);
        return parse(doc);
    public static NSObject parse(final byte[] bytes) throws ParserConfigurationException, ParseException, SAXException, PropertyListFormatException, IOException {
        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
        return parse(bis);
    public static NSObject parse(InputStream is) throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException {
        DocumentBuilder docBuilder = getDocBuilder();

        Document doc = docBuilder.parse(is);
        return parse(doc);
Fixed code:       <fileset dir="${build}/classes"/>
      <fileset dir="${source}" includes="**/*.dtd"/>
<!ENTITY % plistObject "(array | data | date | dict | real | integer | string | true | false )" >
<!ELEMENT plist %plistObject;>
<!ATTLIST plist version CDATA "1.0" >

<!-- Collections -->
<!ELEMENT array (%plistObject;)*>
<!ELEMENT dict (key, %plistObject;)*>
<!ELEMENT key (#PCDATA)>

<!--- Primitive types -->
<!ELEMENT string (#PCDATA)>
<!ELEMENT data (#PCDATA)> <!-- Contents interpreted as Base-64 encoded -->
<!ELEMENT date (#PCDATA)> <!-- Contents should conform to a subset of ISO 8601 (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'.  Smaller units may be omitted with a loss of precision) -->

<!-- Numerical primitives -->
<!ELEMENT true EMPTY>  <!-- Boolean constant true -->
<!ELEMENT false EMPTY> <!-- Boolean constant false -->
<!ELEMENT real (#PCDATA)> <!-- Contents should represent a floating point number matching ("+" | "-")? d+ ("."d*)? ("E" ("+" | "-") d+)? where d is a digit 0-9.  -->
<!ELEMENT integer (#PCDATA)> <!-- Contents should represent a (possibly signed) integer number in base 10 -->
import java.io.FileInputStream;
    private static final DocumentBuilderFactory FACTORY = DocumentBuilderFactory.newInstance();
    static {
        try {
            FACTORY.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
            Float v1_7 = new Float("1.7");
            Float javaVersion = new Float(System.getProperty("java.specification.version"));
            if (javaVersion.compareTo(v1_7) >= 0) {
                FACTORY.setFeature("http://xml.org/sax/features/external-general-entities", false);
                FACTORY.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            }
            FACTORY.setXIncludeAware(false);
            FACTORY.setExpandEntityReferences(false);
            FACTORY.setNamespaceAware(false);
            FACTORY.setIgnoringComments(true);
            FACTORY.setCoalescing(true);
            FACTORY.setValidating(false);
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        }
    public static synchronized DocumentBuilder getDocBuilder() throws ParserConfigurationException {
        DocumentBuilder builder = FACTORY.newDocumentBuilder();
        builder.setEntityResolver(new PlistDTDResolver());
        return builder;
    public static NSObject parse(File f)
                throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException {
        return parse(getDocBuilder().parse(new FileInputStream(f)));
    public static NSObject parse(final byte[] bytes)
                throws ParserConfigurationException, ParseException, SAXException, PropertyListFormatException, IOException {

        return parse(new ByteArrayInputStream(bytes));
    public static NSObject parse(InputStream is)
                throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException {
        return parse(getDocBuilder().parse(is));

    /**
     * Resolves only the Apple PLIST DTD.
     */
    static class PlistDTDResolver implements EntityResolver {
        private static final String PLIST_SYSTEMID_1 = "-//Apple Computer//DTD PLIST 1.0//EN";
        private static final String PLIST_SYSTEMID_2 = "-//Apple//DTD PLIST 1.0//EN";

        PlistDTDResolver() {
        }

        // Implement EntityResolver

        public InputSource resolveEntity(String publicId, String systemId) {
            if (PLIST_SYSTEMID_1.equals(publicId) || PLIST_SYSTEMID_2.equals(publicId)) {
                return new InputSource(XMLPropertyListParser.class.getResourceAsStream("PropertyList-1.0.dtd"));
            }
            return null;
        }
    }